HEX
Server: LiteSpeed
System: Linux server240.web-hosting.com 4.18.0-553.45.1.lve.el8.x86_64 #1 SMP Wed Mar 26 12:08:09 UTC 2025 x86_64
User: creaqbdc (8964)
PHP: 8.0.30
Disabled: NONE
Upload Files
File: //proc/self/cwd/wp-content/plugins/essential-blocks/includes/Blocks/TableOfContents.php
<?php

namespace EssentialBlocks\Blocks;

use EssentialBlocks\Core\Block;

class TableOfContents extends Block
{
    protected $frontend_scripts = [ 'essential-blocks-table-of-contents-block-frontend' ];
    protected $frontend_styles  = [ 'essential-blocks-fontawesome' ];

    /**
     * headings that are used, prevent duplicates.
     *
     * @var null
     */
    public static $the_headings = null;

    /**
     * Post content.
     *
     * @var string
     */
    public static $output_content = '';
    /**
     * Default attributes
     *
     * @var array
     */
    protected $default_attributes;

    public function __construct()
    {
        $this->default_attributes = [
            "title"              => esc_html__( "Table of Contents", "essential-blocks" ),
            "titleTag"           => "h2",
            "collapsible"        => false,
            "initialCollapse"    => false,
            "displayTitle"       => true,
            "isSmooth"           => true,
            "seperator"          => false,
            "isSticky"           => false,
            "contentAlign"       => "left",
            "scrollTarget"       => "scroll_to_toc",
            "stickyPosition"     => "left",
            "enableCopyLink"     => false,
            "showListSeparator"  => false,
            "scrollToTop"        => false,
            "scrollToTopIcon"    => "fas fa-angle-up",
            "stickyHideOnMobile" => false,
            "hideOnDesktop"      => false,
            "hideOnTab"          => false,
            "hideOnMobile"       => false,
            "topOffset"          => "",
            "listStyle"          => "ul",
            "itemCollapsed"      => false,
            "preset"             => "style-1",
            "enableListStyle"    => false,
            "enableHighlight"    => false
         ];
    }

    /**
     * Unique name of the block.
     * @return string
     */
    public function get_name()
    {
        return 'table-of-contents';
    }

    /**
     * Register all other scripts
     * @return void
     */
    public function register_scripts()
    {
        $this->assets_manager->register(
            'table-of-contents-block-frontend',
            $this->path() . '/frontend.js',
            [ 'clipboard', 'essential-blocks-controls-frontend' ]
        );
    }

    /**
     * Get Editor Type
     *
     * @return string
     */
    public function get_editor_type()
    {
        global $pagenow;
        $editor_type = "";
        if ( $pagenow == 'post-new.php' || $pagenow == 'post.php' ) {
            $editor_type = 'edit-post';
        } elseif ( $pagenow == 'site-editor.php' || ( $pagenow == 'themes.php' && isset( $_GET[ 'page' ] ) && $_GET[ 'page' ] == 'gutenberg-edit-site' ) ) {
            $editor_type = 'edit-site';
        } elseif ( $pagenow == 'widgets.php' ) {
            $editor_type = 'edit-widgets';
        }
        return $editor_type;
    }

    /**
     * Check if a array all values are false
     *
     * @return boolean
     */
    public function areAllFalse( $array )
    {
        return array_reduce( $array, function ( $carry, $item ) {
            return $carry && ( $item === false );
        }, true );
    }

    /**
     * Generate TOC
     *
     * @param array $data
     */
    public function generate_toc( $data, $listStyle, $itemCollapsed )
    {
        $toc   = "<$listStyle class='eb-toc__list'>";
        $stack = [  ];

        for ( $i = 0; $i < count( $data ); $i++ ) {
            $level   = $data[ $i ][ 'level' ];
            $content = $data[ $i ][ 'content' ];
            $link    = $data[ $i ][ 'link' ];

            while ( count( $stack ) > 0 && $stack[ count( $stack ) - 1 ][ 'level' ] >= $level ) {
                array_pop( $stack );
                $toc .= "</li></$listStyle>";
            }

            $toc .= "<li><a href=\"#$link\">$content</a>";

            if ( $i < count( $data ) - 1 && $data[ $i + 1 ][ 'level' ] > $level ) {
                if ( $itemCollapsed == 'true' && ! count( $stack ) ) {
                    $toc .= '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
                    <path d="M6.75 9.75L9 7.5L11.25 9.75" stroke="#252D3B" stroke-linecap="round" stroke-linejoin="round"/>
                    <path d="M9 15.75C9.88642 15.75 10.7642 15.5754 11.5831 15.2362C12.4021 14.897 13.1462 14.3998 13.773 13.773C14.3998 13.1462 14.897 12.4021 15.2362 11.5831C15.5754 10.7642 15.75 9.88642 15.75 9C15.75 8.11358 15.5754 7.23583 15.2362 6.41689C14.897 5.59794 14.3998 4.85383 13.773 4.22703C13.1462 3.60023 12.4021 3.10303 11.5831 2.76381C10.7642 2.42459 9.88642 2.25 9 2.25C7.20979 2.25 5.4929 2.96116 4.22703 4.22703C2.96116 5.4929 2.25 7.20979 2.25 9C2.25 10.7902 2.96116 12.5071 4.22703 13.773C5.4929 15.0388 7.20979 15.75 9 15.75Z" stroke="#252D3B" stroke-linecap="round" stroke-linejoin="round"/>
                    </svg>';
                }
                $toc .= "<$listStyle class='eb-toc__list'>";
                array_push( $stack, [ 'level' => $level, 'content' => $content, 'link' => $link ] );
            }
        }
        while ( count( $stack ) > 0 ) {
            array_pop( $stack );
            $toc .= "</li></$listStyle>";
        }

        $toc .= "</$listStyle>";

        return $toc;
    }

    /**
     * Generate headers from content
     */
    public function getHeadersFromContent( $visibleHeaders, $postContent )
    {
        if ( empty( $postContent ) ) {
            return [  ];
        }

        $dom = new \DOMDocument();
        libxml_use_internal_errors( true );
        $dom->loadHTML( '<html><head><meta http-equiv="content-type" content="text/html; charset=utf-8"></head><body>' . $postContent . '</body></html>' );
        libxml_use_internal_errors( false );

        $queryArray = [ "h1", "h2", "h3", "h4", "h5", "h6" ];
        if ( isset( $visibleHeaders ) ) {
            $queryArray = [  ];
            if ( $visibleHeaders[ 0 ] ) {
                $queryArray[  ] = "self::h1";
            }
            if ( $visibleHeaders[ 1 ] ) {
                $queryArray[  ] = "self::h2";
            }
            if ( $visibleHeaders[ 2 ] ) {
                $queryArray[  ] = "self::h3";
            }
            if ( $visibleHeaders[ 3 ] ) {
                $queryArray[  ] = "self::h4";
            }
            if ( $visibleHeaders[ 4 ] ) {
                $queryArray[  ] = "self::h5";
            }
            if ( $visibleHeaders[ 5 ] ) {
                $queryArray[  ] = "self::h6";
            }
        }

        $queryString = implode( ' or ', $queryArray );
        $queryString = '//*[' . $queryString . ']';

        if ( ! $this->areAllFalse( $visibleHeaders ) ) {
            $xpath           = new \DOMXpath( $dom );
            $headingElements = iterator_to_array( $xpath->query( $queryString ) );
            return $this->getHeadingsFromHeadingElements( $headingElements );
        }

        return [  ];
    }

    /**
     * generate heading from headings elements
     */
    public function getHeadingsFromHeadingElements( $headingElements )
    {
        $headings = [  ];
        foreach ( $headingElements as $index => $heading ) {
            $level = null;
            switch ( $heading->tagName ) {
                case "h1":
                    $level = 1;
                    break;
                case "h2":
                    $level = 2;
                    break;
                case "h3":
                    $level = 3;
                    break;
                case "h4":
                    $level = 4;
                    break;
                case "h5":
                    $level = 5;
                    break;
                case "h6":
                    $level = 6;
                    break;
            }

            $value          = apply_filters( 'eb_dynamic_tag_value', $heading->textContent, '', true );
            $value          = empty( $value ) ? $heading->textContent : $value;
            $heading_string = $this->parseTocSlug( wp_strip_all_tags( $value ) );

            $headings[  ] = [
                "level"   => $level,
                "content" => $value,
                "text"    => $value,
                "link"    => preg_match( '/^[A-Za-z0-9\-\_]+$/', $heading_string ) === 1 ? $heading_string : "eb-table-content-$index"
             ];
        }

        return $headings;
    }

    /**
     * parse slug
     */
    public function parseTocSlug( $slug )
    {
        if ( ! $slug ) {
            return $slug;
        }
        // TODO: have to remove accent function
        $parsedSlug = strtolower( $slug );
        $parsedSlug = preg_replace( '/&(amp;)/', '', $parsedSlug ); // Remove &
        $parsedSlug = preg_replace( '/&(mdash;)/', '', $parsedSlug ); // Remove long dash
        $parsedSlug = preg_replace( '/[\x{2013}\x{2014}]/u', '', $parsedSlug ); // Remove long dash
        $parsedSlug = preg_replace( '/[&]nbsp[;]/i', '-', $parsedSlug ); // Replace inseccable spaces
        $parsedSlug = preg_replace( '/\s+/', '-', $parsedSlug ); // Replace spaces with -
        $parsedSlug = preg_replace( '/[&\/\\#,^!+()$~%.\'":*?<>{}@‘’”“]/', '', $parsedSlug ); // Remove special chars
        $parsedSlug = preg_replace( '/\-+/', '-', $parsedSlug ); // Replace multiple - with single -
        $parsedSlug = preg_replace( '/^-+/', '', $parsedSlug ); // Trim - from start of text
        $parsedSlug = preg_replace( '/-+$/', '', $parsedSlug ); // Trim - from end of text

        return urldecode( rawurlencode( $parsedSlug ) );
    }

    /**
     * Callback function for wp_kses_allowed_html
     *
     * @param array $tags
     *
     * @return array
     */
    public function allowed_html( $tags )
    {
        $tags = is_array( $tags ) ? $tags : [  ];
        return array_merge( $tags, [
            'svg'  => [
                'xmlns'        => true,
                'version'      => true,
                'width'        => true,
                'height'       => true,
                'viewBox'      => true,
                'fill'         => true,
                'stroke'       => true,
                'stroke-width' => true,
                'xmlns:xlink'  => true
             ],
            'g'    => [  ],
            'path' => [
                'd' => true
             ]
         ] );
    }

    /**
     * Render Table of Contents Block
     *
     * @param array $attributes the blocks attribtues.
     */
    public function render_callback( $attributes, $content )
    {
        $the_post = get_post();

        if ( ! $the_post ) {
            return;
        }

        $attributes         = wp_parse_args( $attributes, $this->default_attributes );
        $blockId            = esc_attr( $attributes[ 'blockId' ] );
        $scrollToTop        = $attributes[ 'scrollToTop' ] ? 'true' : 'false';
        $scrollToTopIcon    = $attributes[ 'scrollToTopIcon' ];
        $listStyle          = $attributes[ 'listStyle' ];
        $collapsible        = $attributes[ 'collapsible' ] ? 'true' : 'false';
        $initialCollapse    = $attributes[ 'initialCollapse' ] ? 'true' : 'false';
        $stickyHideOnMobile = $attributes[ 'stickyHideOnMobile' ] ? 'true' : 'false';
        $isSticky           = $attributes[ 'isSticky' ] ? 'true' : 'false';
        $stickyPosition     = $attributes[ 'stickyPosition' ];
        $scrollTarget       = $attributes[ 'scrollTarget' ];
        $enableCopyLink     = $attributes[ 'enableCopyLink' ] ? 'true' : 'false';
        $displayTitle       = $attributes[ 'displayTitle' ] ? 'true' : 'false';
        $title              = $attributes[ 'title' ];
        $titleTag           = $attributes[ 'titleTag' ];
        $isSmooth           = $attributes[ 'isSmooth' ] ? 'true' : 'false';
        $itemCollapsed      = $attributes[ 'itemCollapsed' ] ? 'true' : 'false';
        $topOffset          = $attributes[ 'topOffset' ];
        $preset             = $attributes[ 'preset' ];
        $enableListStyle    = $attributes[ 'enableListStyle' ];
        $hideOnDesktop      = $attributes[ 'hideOnDesktop' ] ? 'true' : 'false';
        $hideOnTab          = $attributes[ 'hideOnTab' ] ? 'true' : 'false';
        $hideOnMobile       = $attributes[ 'hideOnMobile' ] ? 'true' : 'false';
        $visibleHeaders     = isset( $attributes[ 'visibleHeaders' ] ) ? $attributes[ 'visibleHeaders' ] : array_fill( 0, 6, true );
        $content            = html_entity_decode( preg_replace( "~<!--(.*?)-->~s", "", $the_post->post_content ) );
        $headers            = $this->getHeadersFromContent( $visibleHeaders, wp_kses_post( $content ) );
        $enableHighlight    = $attributes[ 'enableHighlight' ] ? 'true' : 'false';
        $deleteHeaderList   = isset( $attributes[ 'deleteHeaderList' ] ) ? $attributes[ 'deleteHeaderList' ] : [  ];
        $classHook          = isset( $attributes[ 'classHook' ] ) ? $attributes[ 'classHook' ] : '';

        $container_class     = [  ];
        $container_class[  ] = 'eb-toc-container ' . $blockId;
        $container_class[  ] = $isSticky == 'true' ? 'eb-toc-sticky-' . $stickyPosition : '';
        $container_class[  ] = $isSticky == 'true' ? 'eb-toc-is-sticky' : 'eb-toc-is-not-sticky';
        $container_class[  ] = $collapsible == 'true' ? 'eb-toc-collapsible' : 'eb-toc-not-collapsible';
        $container_class[  ] = $initialCollapse == 'true' ? 'eb-toc-initially-collapsed' : 'eb-toc-initially-not-collapsed';
        $container_class[  ] = $scrollToTop ? 'eb-toc-scrollToTop' : 'eb-toc-not-scrollToTop';
        $container_class[  ] = $preset;
        $container_class[  ] = $enableListStyle === false ? 'list-style-none' : '';

        $wrapper_class     = [  ];
        $wrapper_class[  ] = $collapsible == 'true' && $initialCollapse == 'true' && $isSticky == 'false' ? 'hide-content' : '';

        $wrapper_attributes = get_block_wrapper_attributes(
            [
                'class' => 'root-' . $blockId
             ]
        );

        $output = "";
        $output .= '<div ' . wp_kses_data( $wrapper_attributes ) . '>';
        $output .= '<div class="eb-parent-wrapper eb-parent-' . $blockId . ' ' . $classHook . '">';
        $output .= '<div class="' . implode( " ", $container_class ) . '"
                data-scroll-top="' . $scrollToTop . '"
                data-scroll-top-icon="' . $scrollToTopIcon . '"
                data-collapsible="' . $collapsible . '"
                data-sticky-hide-mobile="' . $stickyHideOnMobile . '"
                data-sticky="' . $isSticky . '"
                data-scroll-target="' . $scrollTarget . '"
                data-copy-link="' . $enableCopyLink . '"
                data-editor-type="' . $this->get_editor_type() . '"
                data-hide-desktop="' . $hideOnDesktop . '"
                data-hide-tab="' . $hideOnTab . '"
                data-hide-mobile="' . $hideOnMobile . '"
                data-itemCollapsed="' . $itemCollapsed . '"
                data-highlight-scroll="' . $enableHighlight . '"
                >';
        $output .= '<div class="eb-toc-header">';
        if ( $isSticky == 'true' ) {
            $output .= '<span class="eb-toc-close eb-toc-sticky-' . $stickyPosition . '">';
            $output .= '</span>';
        }
        if ( $displayTitle == 'true' ) {
            $output .= '<' . $titleTag . ' class="eb-toc-title">' . $title . '</' . $titleTag . '>';
        }

        $output .= '</div>'; // header
        $output .= '<div class="eb-toc-wrapper ' . implode( " ", $wrapper_class ) . '"
        data-headers="' . htmlspecialchars( wp_json_encode( $headers ), ENT_QUOTES, 'UTF-8' ) . '"
        data-visible="' . wp_json_encode( $visibleHeaders ) . '"
        data-delete-headers="' . htmlspecialchars( wp_json_encode( $deleteHeaderList ), ENT_QUOTES, 'UTF-8' ) . '"
        data-smooth="' . $isSmooth . '"
        data-top-offset="' . $topOffset . '"
        >';

        if ( $visibleHeaders && count( $headers ) > 0 && count( array_filter( $headers, function ( $header ) use ( $visibleHeaders ) {
            return isset( $visibleHeaders[ $header[ 'level' ] - 1 ] );
        } ) ) > 0 ) {
            $newHeaders = [  ];
            foreach ( $headers as $index => $item ) {
                if (
                    isset( $deleteHeaderList ) &&
                    is_array( $deleteHeaderList ) &&
                    count( $deleteHeaderList ) > 0 &&
                    isset( $deleteHeaderList[ $index ] ) &&
                    isset( $deleteHeaderList[ $index ][ "isDelete" ] ) &&
                    $deleteHeaderList[ $index ][ "isDelete" ] === false
                ) {
                    $newHeaders[  ] = $headers[ $index ];
                }
            }

            $output .= '<div class="eb-toc__list-wrap">';
            $output .= count( $newHeaders ) > 0 ? $this->generate_toc( $newHeaders, $listStyle, $itemCollapsed ) : $this->generate_toc( $headers, $listStyle, $itemCollapsed );
            $output .= '</div>';
        }

        $output .= '</div>'; // wrapper
        $stickyPositionClass = $isSticky ? " eb-toc-button-$stickyPosition" : '';
        if ( 'false' !== $isSticky ) {
            $output .= '<button class="eb-toc-button ' . $stickyPositionClass . '">';
            if ( $displayTitle ) {
                $output .= '<div>' . $title . '</div>';
            }
            $output .= '</button>';
        }
        $output .= '</div>'; // container
        $output .= '</div>'; // parent wrapper
        $output .= "</div>"; // block

        add_filter( 'wp_kses_allowed_html', [ $this, 'allowed_html' ] );
        $content = wp_kses_post( $output );
        remove_filter( 'wp_kses_allowed_html', [ $this, 'allowed_html' ] );

        return $content;
    }
}