([^`]*?)<\/div>/', $content, $matches ); if ( ! empty( $matches[1] ) ) { $parsed_block['attrs']['headingTitle'] = $matches[1]; } } return $parsed_block; } /** * Delete toc meta. * * @access public * * @since 1.23.0 * @param int $post_id Post ID. * @param object $post Post object. * @param boolean $update Whether this is an existing post being updated. */ public function delete_toc_meta( $post_id, $post, $update ) { delete_post_meta( $post_id, '_uagb_toc_options' ); } /** * Extracts heading content, id, and level from the given post content. * * @since 1.23.0 * @access public * * @param string $content The post content to extract headings from. * * @return array The list of headings. */ public function table_of_contents_get_headings_from_content( $content ) { /* phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase */ // Disabled because of PHP DOMDocument and DOMXPath APIs using camelCase. // Create a document to load the post content into. $doc = new DOMDocument( '1.0', 'UTF-8' ); // Enable user error handling for the HTML parsing. HTML5 elements aren't // supported (as of PHP 7.4) and There's no way to guarantee that the markup // is valid anyway, so we're just going to ignore all errors in parsing. // Nested heading elements will still be parsed. // The lack of HTML5 support is a libxml2 issue: // https://bugzilla.gnome.org/show_bug.cgi?id=761534. libxml_use_internal_errors( true ); // Parse the post content into an HTML document. $doc->loadHTML( // loadHTML expects ISO-8859-1, so we need to convert the post content to // that format. We use htmlentities to encode Unicode characters not // supported by ISO-8859-1 as HTML entities. However, this function also // converts all special characters like < or > to HTML entities, so we use // htmlspecialchars_decode to decode them. '' . $content . '' ); // We're done parsing, so we can disable user error handling. This also // clears any existing errors, which helps avoid a memory leak. libxml_use_internal_errors( false ); // IE11 treats template elements like divs, so to avoid extracting heading // elements from them, we first have to remove them. // We can't use foreach directly on the $templates DOMNodeList because it's a // dynamic list, and removing nodes confuses the foreach iterator. So // instead, we convert the iterator to an array and then iterate over that. if ( ! isset( $doc->documentElement ) || ! is_object( $doc->documentElement ) ) { return array(); } $templates = iterator_to_array( $doc->documentElement->getElementsByTagName( 'template' ) ); foreach ( $templates as $template ) { $template->parentNode->removeChild( $template ); } $xpath = new DOMXPath( $doc ); $tags = array( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div' ); // Delete $tags[$s].uagb-toc-hide-heading from doc. foreach ( $tags as $tag ) { $query = sprintf( '//%s[contains(attribute::class, "uagb-toc-hide-heading")]', $tag ); foreach ( $xpath->query( $query ) as $e ) { $e->parentNode->removeChild( $e ); } } // Get all non-empty heading elements in the post content. $headings = iterator_to_array( $xpath->query( '//*[self::h1 or self::h2 or self::h3 or self::h4 or self::h5 or self::h6]' ) ); return array_map( function ( $heading ) { $exclude_heading = null; if ( isset( $heading->attributes ) ) { $class_name = $heading->attributes->getNamedItem( 'class' ); if ( null !== $class_name && '' !== $class_name->value ) { $exclude_heading = $class_name->value; } } $mapping_header = 0; if ( 'uagb-toc-hide-heading' !== $exclude_heading ) { return array( // A little hacky, but since we know at this point that the tag will // be an h1-h6, we can just grab the 2nd character of the tag name // and convert it to an integer. Should be faster than conditionals. 'level' => (int) $heading->nodeName[1], 'id' => $this->clean( $heading->textContent ), 'content' => wp_strip_all_tags( $heading->textContent ), 'depth' => intval( substr( $heading->tagName, 1 ) ), ); } }, $headings ); /* phpcs:enable */ } /** * Clean up heading content. * * @since 1.23.0 * @access public * * @param string $string The post content to extract headings from. * * @return string $string. */ public function clean( $string ) { $string = preg_replace( '/[\x00-\x1F\x7F]*/u', '', $string ); $string = str_replace( array( '&', ' ' ), ' ', $string ); // Remove all except alphbets, space, `-`,`_` and latin characters. $string = preg_replace( '/[^a-zA-Z0-9\p{L} _-]/u', '', $string ); // Convert space characters to an `_` (underscore). $string = preg_replace( '/\s+/', '_', $string ); // Replace multiple `_` (underscore) with a single `-` (hyphen). $string = preg_replace( '/_+/', '-', $string ); // Replace multiple `-` (hyphen) with a single `-` (hyphen). $string = preg_replace( '/-+/', '-', $string ); // Remove trailing `-` and `_`. $string = trim( $string, '-_' ); if ( empty( $string ) ) { $string = 'toc_' . uniqid(); } return strtolower( $string ); // Replaces multiple hyphens with single one. } /** * Converts a flat list of heading parameters to a hierarchical nested list * based on each header's immediate parent's level. * * @since 1.23.0 * @access public * * @param array $heading_list Flat list of heading parameters to nest. * @param int $index The current list index. * * @return array A hierarchical nested list of heading parameters. */ public function table_of_contents_linear_to_nested_heading_list( $heading_list, $index = 0 ) { $nested_heading_list = array(); foreach ( $heading_list as $key => $heading ) { if ( ! is_null( $heading_list[ $key ] ) ) { $nested_heading_list[] = array( 'heading' => $heading, 'index' => $index + $key, 'children' => null, ); } } return $nested_heading_list; } /** * Renders the heading list of the UAGB Table Of Contents block. * * @since 1.23.0 * @access public * * @param array $nested_heading_list Nested list of heading data. * @param string $page_url URL of the page the block belongs to. * @param array $attributes array of attributes. * * @return string The heading list rendered as HTML. */ public function table_of_contents_render_list( $nested_heading_list, $page_url, $attributes ) { $toc = '
    '; $last_level = ''; $parent_level = ''; $first_level = ''; $current_depth = 0; $depth_array = array( 1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 0, 6 => 0, ); foreach ( $nested_heading_list as $anchor => $heading ) { $level = $heading['heading']['level']; $title = $heading['heading']['content']; $id = $heading['heading']['id']; if ( 0 === $anchor ) { $first_level = $level; } if ( $level < $first_level ) { continue; } if ( empty( $parent_level ) || $level < $parent_level ) { $parent_level = $level; } if ( ! empty( $last_level ) ) { if ( $level > $last_level ) { $toc .= '', $closing ); $current_depth = absint( $current_depth - $closing ); } elseif ( $level === $parent_level ) { $toc .= str_repeat( '', $closing ); $toc .= ''; } } } $toc .= sprintf( '
  1. %s', esc_attr( $id ), esc_html( $title ) ); $last_level = $level; } $toc .= str_repeat( '', $current_depth ); $toc .= '
'; return $toc; } /** * Filters the Headings according to Mapping Headers Array. * * @since 1.24.0 * @access public * * @param array $headings Headings. * @param array $mapping_headers_array Mapping Headers. * * @return array FIltered Headings Array.. */ public function filter_headings_by_mapping_headers( $headings, $mapping_headers_array ) { $filtered_headings = array(); foreach ( $headings as $heading ) { $mapping_header = 0; foreach ( $mapping_headers_array as $key => $value ) { if ( $mapping_headers_array[ $key ] ) { $mapping_header = ( $key + 1 ); } if ( isset( $heading ) && $mapping_header === $heading['level'] ) { $filtered_headings[] = $heading; break; } } } return $filtered_headings; } /** * Get the Reusable Headings Array. * * @since 2.0.14 * @access public * * @param array $blocks_array Block Array. * * @return array $final_reusable_array Heading Array. */ public function toc_recursive_reusable_heading( $blocks_array ) { $final_reusable_array = array(); foreach ( $blocks_array as $key => $block ) { if ( 'core/block' === $blocks_array[ $key ]['blockName'] ) { if ( $blocks_array[ $key ]['attrs'] ) { $reusable_block = get_post( $blocks_array[ $key ]['attrs']['ref'] ); $reusable_heading = $this->table_of_contents_get_headings_from_content( $reusable_block->post_content ); if ( isset( $reusable_heading[0] ) ) { $final_reusable_array = array_merge( $final_reusable_array, $reusable_heading ); } } } else { if ( 'core/block' !== $blocks_array[ $key ]['blockName'] ) { $inner_block_reusable_array = $this->toc_recursive_reusable_heading( $blocks_array[ $key ]['innerBlocks'] ); $final_reusable_array = array_merge( $final_reusable_array, $inner_block_reusable_array ); } } } return $final_reusable_array; } /** * Renders the UAGB Table Of Contents block. * * @since 1.23.0 * @access public * * @param array $attributes Block attributes. * @param string $content Block default content. * @param WP_Block $block Block instance. * * @return string Rendered block HTML. */ public function render_table_of_contents( $attributes, $content, $block ) { global $post; $result = array(); if ( ! isset( $post->ID ) ) { return ''; } $uagb_toc_options = get_post_meta( $post->ID, '_uagb_toc_options', true ); $uagb_toc_version = ! empty( $uagb_toc_options['_uagb_toc_version'] ) ? $uagb_toc_options['_uagb_toc_version'] : ''; $uagb_toc_heading_content = ! empty( $uagb_toc_options['_uagb_toc_headings'] ) ? $uagb_toc_options['_uagb_toc_headings'] : ''; if ( empty( $uagb_toc_heading_content ) || UAGB_ASSET_VER !== $uagb_toc_version ) { global $_wp_current_template_content; // If the current template contents exist, use that - else get the content from the post ID. if ( $_wp_current_template_content ) { $content = $_wp_current_template_content; } else { $content = get_post( $post->ID )->post_content; } $uagb_toc_heading_content = $this->table_of_contents_get_headings_from_content( $content ); $blocks = parse_blocks( $content ); $uagb_toc_reusable_heading_content = $this->toc_recursive_reusable_heading( $blocks ); $uagb_toc_heading_content = array_merge( $uagb_toc_heading_content, $uagb_toc_reusable_heading_content ); $meta_array = array( '_uagb_toc_version' => UAGB_ASSET_VER, '_uagb_toc_headings' => $uagb_toc_heading_content, ); update_post_meta( $post->ID, '_uagb_toc_options', $meta_array ); } $uagb_toc_heading_content = $this->filter_headings_by_mapping_headers( $uagb_toc_heading_content, $attributes['mappingHeaders'] ); $mapping_header_func = function( $value ) { return $value; }; $desktop_class = ''; $tab_class = ''; $mob_class = ''; if ( array_key_exists( 'UAGHideDesktop', $attributes ) || array_key_exists( 'UAGHideTab', $attributes ) || array_key_exists( 'UAGHideMob', $attributes ) ) { $desktop_class = ( isset( $attributes['UAGHideDesktop'] ) ) ? 'uag-hide-desktop' : ''; $tab_class = ( isset( $attributes['UAGHideTab'] ) ) ? 'uag-hide-tab' : ''; $mob_class = ( isset( $attributes['UAGHideMob'] ) ) ? 'uag-hide-mob' : ''; } $zindex_desktop = ''; $zindex_tablet = ''; $zindex_mobile = ''; $zindex_wrap = array(); $zindex_extention_enabled = ( isset( $attributes['zIndex'] ) || isset( $attributes['zIndexTablet'] ) || isset( $attributes['zIndexMobile'] ) ); if ( $zindex_extention_enabled ) { $zindex_desktop = ( isset( $attributes['zIndex'] ) ) ? '--z-index-desktop:' . $attributes['zIndex'] . ';' : false; $zindex_tablet = ( isset( $attributes['zIndexTablet'] ) ) ? '--z-index-tablet:' . $attributes['zIndexTablet'] . ';' : false; $zindex_mobile = ( isset( $attributes['zIndexMobile'] ) ) ? '--z-index-mobile:' . $attributes['zIndexMobile'] . ';' : false; if ( $zindex_desktop ) { array_push( $zindex_wrap, $zindex_desktop ); } if ( $zindex_tablet ) { array_push( $zindex_wrap, $zindex_tablet ); } if ( $zindex_mobile ) { array_push( $zindex_wrap, $zindex_mobile ); } } $wrap = array( 'wp-block-uagb-table-of-contents', 'uagb-toc__align-' . $attributes['align'], 'uagb-toc__columns-' . $attributes['tColumnsDesktop'], ( ( true === $attributes['initialCollapse'] ) ? 'uagb-toc__collapse' : '' ), 'uagb-block-' . $attributes['block_id'], ( isset( $attributes['className'] ) ) ? $attributes['className'] : '', $desktop_class, $tab_class, $mob_class, $zindex_extention_enabled ? 'uag-blocks-common-selector' : '', ); ob_start(); ?>
0 && count( array_filter( $attributes['mappingHeaders'], $mapping_header_func ) ) > 0 ) { ?>
table_of_contents_render_list( $this->table_of_contents_linear_to_nested_heading_list( $uagb_toc_heading_content ), get_permalink( $post->ID ), $attributes ) ); ?>

array_merge( array( 'block_id' => array( 'type' => 'string', 'default' => 'not_set', ), 'classMigrate' => array( 'type' => 'boolean', 'default' => false, ), 'headingTitleString' => array( 'type' => 'string', ), 'disableBullets' => array( 'type' => 'boolean', 'default' => false, ), 'makeCollapsible' => array( 'type' => 'boolean', 'default' => false, ), 'initialCollapse' => array( 'type' => 'boolean', 'default' => false, ), 'icon' => array( 'type' => 'string', 'default' => 'angle-down', ), 'iconSize' => array( 'type' => 'number', ), 'iconColor' => array( 'type' => 'string', ), 'bulletColor' => array( 'type' => 'string', ), 'align' => array( 'type' => 'string', 'default' => 'left', ), 'headingAlignment' => array( 'type' => 'string', 'default' => 'left', ), 'heading' => array( 'type' => 'string', 'selector' => '.uagb-toc__title', 'default' => __( 'Table Of Contents', 'ultimate-addons-for-gutenberg' ), ), 'headingTitle' => array( 'type' => 'string', 'default' => __( 'Table Of Contents', 'ultimate-addons-for-gutenberg' ), ), 'smoothScroll' => array( 'type' => 'boolean', 'default' => true, ), 'smoothScrollOffset' => array( 'type' => 'number', 'default' => 30, ), 'scrollToTop' => array( 'type' => 'boolean', 'default' => false, ), 'scrollToTopColor' => array( 'type' => 'string', ), 'scrollToTopBgColor' => array( 'type' => 'string', ), 'tColumnsDesktop' => array( 'type' => 'number', 'default' => 1, ), 'tColumnsTablet' => array( 'type' => 'number', 'default' => 1, ), 'tColumnsMobile' => array( 'type' => 'number', 'default' => 1, ), 'mappingHeaders' => array( 'type' => 'array', 'default' => $mapping_headers_array, ), // Color. 'backgroundColor' => array( 'type' => 'string', 'default' => '#eee', ), 'linkColor' => array( 'type' => 'string', 'default' => '#333', ), 'linkHoverColor' => array( 'type' => 'string', ), 'headingColor' => array( 'type' => 'string', ), // Padding. 'topPaddingTablet' => array( 'type' => 'number', 'default' => '', ), 'bottomPaddingTablet' => array( 'type' => 'number', 'default' => '', ), 'leftPaddingTablet' => array( 'type' => 'number', 'default' => '', ), 'rightPaddingTablet' => array( 'type' => 'number', 'default' => '', ), 'topPaddingMobile' => array( 'type' => 'number', 'default' => '', ), 'bottomPaddingMobile' => array( 'type' => 'number', 'default' => '', ), 'leftPaddingMobile' => array( 'type' => 'number', 'default' => '', ), 'rightPaddingMobile' => array( 'type' => 'number', 'default' => '', ), 'vPaddingDesktop' => array( 'type' => 'number', 'default' => 30, ), 'hPaddingDesktop' => array( 'type' => 'number', 'default' => 30, ), 'vPaddingTablet' => array( 'type' => 'number', ), 'hPaddingTablet' => array( 'type' => 'number', ), 'vPaddingMobile' => array( 'type' => 'number', ), 'hPaddingMobile' => array( 'type' => 'number', ), // Margin. 'vMarginDesktop' => array( 'type' => 'number', ), 'hMarginDesktop' => array( 'type' => 'number', ), 'vMarginTablet' => array( 'type' => 'number', ), 'hMarginTablet' => array( 'type' => 'number', ), 'vMarginMobile' => array( 'type' => 'number', ), 'hMarginMobile' => array( 'type' => 'number', ), 'marginTypeDesktop' => array( 'type' => 'string', 'default' => 'px', ), 'marginTypeTablet' => array( 'type' => 'string', 'default' => 'px', ), 'marginTypeMobile' => array( 'type' => 'string', 'default' => 'px', ), 'headingBottom' => array( 'type' => 'number', ), 'headingBottomTablet' => array( 'type' => 'number', ), 'headingBottomMobile' => array( 'type' => 'number', ), 'paddingTypeDesktop' => array( 'type' => 'string', 'default' => 'px', ), 'paddingTypeTablet' => array( 'type' => 'string', 'default' => 'px', ), 'paddingTypeMobile' => array( 'type' => 'string', 'default' => 'px', ), // Content Padding. 'contentPaddingDesktop' => array( 'type' => 'number', ), 'contentPaddingTablet' => array( 'type' => 'number', ), 'contentPaddingMobile' => array( 'type' => 'number', ), 'contentPaddingTypeDesktop' => array( 'type' => 'string', 'default' => 'px', ), 'contentPaddingTypeTablet' => array( 'type' => 'string', 'default' => 'px', ), 'contentPaddingTypeMobile' => array( 'type' => 'string', 'default' => 'px', ), // Border. 'borderStyle' => array( 'type' => 'string', 'default' => 'solid', ), 'borderWidth' => array( 'type' => 'number', 'default' => 1, ), 'borderRadius' => array( 'type' => 'number', ), 'borderColor' => array( 'type' => 'string', 'default' => '#333', ), // Typography. // Link Font Family. 'loadGoogleFonts' => array( 'type' => 'boolean', 'default' => false, ), 'fontFamily' => array( 'type' => 'string', 'default' => 'Default', ), 'fontWeight' => array( 'type' => 'string', ), // Link Font Size. 'fontSize' => array( 'type' => 'number', ), 'fontSizeType' => array( 'type' => 'string', 'default' => 'px', ), 'fontSizeTablet' => array( 'type' => 'number', ), 'fontSizeMobile' => array( 'type' => 'number', ), // Link Line Height. 'lineHeightType' => array( 'type' => 'string', 'default' => 'em', ), 'lineHeight' => array( 'type' => 'number', ), 'lineHeightTablet' => array( 'type' => 'number', ), 'lineHeightMobile' => array( 'type' => 'number', ), // Link Font Family. 'headingLoadGoogleFonts' => array( 'type' => 'boolean', 'default' => false, ), 'headingFontFamily' => array( 'type' => 'string', 'default' => 'Default', ), 'headingFontWeight' => array( 'type' => 'string', 'default' => '500', ), // Link Font Size. 'headingFontSize' => array( 'type' => 'number', 'default' => 20, ), 'headingFontSizeType' => array( 'type' => 'string', 'default' => 'px', ), 'headingFontSizeTablet' => array( 'type' => 'number', ), 'headingFontSizeMobile' => array( 'type' => 'number', ), // Link Line Height. 'headingLineHeightType' => array( 'type' => 'string', 'default' => 'em', ), 'headingLineHeight' => array( 'type' => 'number', ), 'headingLineHeightTablet' => array( 'type' => 'number', ), 'headingLineHeightMobile' => array( 'type' => 'number', ), 'emptyHeadingTeaxt' => array( 'type' => 'string', 'default' => __( 'Add a header to begin generating the table of contents', 'ultimate-addons-for-gutenberg' ), ), // Separator. 'separatorStyle' => array( 'type' => 'string', 'default' => 'none', ), 'separatorHeight' => array( 'type' => 'number', 'default' => 1, ), 'separatorHeightType' => array( 'type' => 'string', 'default' => 'px', ), 'separatorSpace' => array( 'type' => 'number', 'default' => 15, ), 'separatorSpaceTablet' => array( 'type' => 'number', 'default' => '', ), 'separatorSpaceMobile' => array( 'type' => 'number', 'default' => '', ), 'separatorSpaceType' => array( 'type' => 'string', 'default' => 'px', ), 'separatorColor' => array( 'type' => 'string', 'default' => '', ), 'separatorHColor' => array( 'type' => 'string', 'default' => '', ), // Overall block alignment. 'overallAlign' => array( 'type' => 'string', 'default' => 'left', ), ) ), 'render_callback' => array( $this, 'render_table_of_contents' ), ) ); } } /** * Prepare if class 'UAGB_Table_Of_Content' exist. * Kicking this off by calling 'get_instance()' method */ UAGB_Table_Of_Content::get_instance(); }