HEX
Server: Apache
System: Linux outside 5.4.8_Algonet_ZeroMAC_0.9.4.8 #6 SMP Mon Feb 3 20:36:07 CET 2020 x86_64
User: server (1002)
PHP: 8.3.20
Disabled: exec,passthru,shell_exec,system,proc_open,popen,curl_multi_exec,parse_ini_file,show_source
Upload Files
File: /home/muoapartman/www/wp-content/plugins/gutenberg/build/block-library/blocks/table-of-contents.php
<?php
/**
 * Server-side rendering of the `core/table-of-contents` block.
 *
 * @package WordPress
 */

/**
 * Extracts heading content, id, page, and level from the given post content.
 *
 * @access private
 *
 * @param string $content       The post content to extract headings from.
 * @param int    $headings_page The page of the post where the headings are
 *                              located.
 *
 * @return array The list of headings.
 */
function gutenberg_gutenberg_block_core_table_of_contents_get_headings_from_content(
	$content,
	$headings_page = 1
) {
	/* 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();

	// 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.
		htmlspecialchars_decode(
			utf8_decode(
				htmlentities(
					'<html><body>' . $content . '</body></html>',
					ENT_COMPAT,
					'UTF-8',
					false
				)
			),
			ENT_COMPAT
		)
	);

	// 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.
	$templates = iterator_to_array(
		$doc->documentElement->getElementsByTagName( 'template' )
	);

	foreach ( $templates as $template ) {
		$template->parentNode->removeChild( $template );
	}

	$xpath = new DOMXPath( $doc );

	// 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][text()!=""]'
		)
	);

	return array_map(
		function ( $heading ) use ( $headings_page ) {
			$id = null;

			if ( isset( $heading->attributes ) ) {
				$id_attribute = $heading->attributes->getNamedItem( 'id' );

				if ( null !== $id_attribute && '' !== $id_attribute->nodeValue ) {
					$id = $id_attribute->nodeValue;
				}
			}

			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'      => $id,
				'page'    => $headings_page,
				'content' => $heading->textContent,
			);
		},
		$headings
	);
	/* phpcs:enable */
}

/**
 * Gets the content, anchor, level, and page of headings from a post. Returns
 * data from all headings in a paginated post if $current_page_only is false;
 * otherwise, returns only data from headings on the current page being
 * rendered.
 *
 * @access private
 *
 * @param int  $post_id           Id of the post to extract headings from.
 * @param bool $current_page_only Whether to include headings from the entire
 *                                post, or just those from the current page (if
 *                                the post is paginated).
 *
 * @return array The list of headings.
 */
function gutenberg_block_core_table_of_contents_get_headings(
	$post_id,
	$current_page_only
) {
	global $multipage, $page, $pages;

	if ( $multipage ) {
		// Creates a list of heading lists, one list per page.
		$pages_of_headings = array_map(
			function( $page_content, $page_index ) {
				return gutenberg_gutenberg_block_core_table_of_contents_get_headings_from_content(
					$page_content,
					$page_index + 1
				);
			},
			$pages,
			array_keys( $pages )
		);

		if ( $current_page_only ) {
			// Return the headings from the current page.
			return $pages_of_headings[ $page - 1 ];
		} else {
			// Concatenate the heading lists into a single array and return it.
			return array_merge( ...$pages_of_headings );
		}
	} else {
		// Only one page, so return headings from entire post_content.
		return gutenberg_gutenberg_block_core_table_of_contents_get_headings_from_content(
			get_post( $post_id )->post_content
		);
	}
}

/**
 * Converts a flat list of heading parameters to a hierarchical nested list
 * based on each header's immediate parent's level.
 *
 * @access private
 *
 * @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.
 */
function gutenberg_block_core_table_of_contents_linear_to_nested_heading_list(
	$heading_list,
	$index = 0
) {
	$nested_heading_list = array();

	foreach ( $heading_list as $key => $heading ) {
		// Make sure we are only working with the same level as the first
		// iteration in our set.
		if ( $heading['level'] === $heading_list[0]['level'] ) {
			// Check that the next iteration will return a value.
			// If it does and the next level is greater than the current level,
			// the next iteration becomes a child of the current iteration.
			if (
				isset( $heading_list[ $key + 1 ] ) &&
				$heading_list[ $key + 1 ]['level'] > $heading['level']
			) {
				// We need to calculate the last index before the next iteration
				// that has the same level (siblings). We then use this last index
				// to slice the array for use in recursion. This prevents duplicate
				// nodes.
				$heading_list_length = count( $heading_list );
				$end_of_slice        = $heading_list_length;
				for ( $i = $key + 1; $i < $heading_list_length; $i++ ) {
					if ( $heading_list[ $i ]['level'] === $heading['level'] ) {
						$end_of_slice = $i;
						break;
					}
				}

				// Found a child node: Push a new node onto the return array with
				// children.
				$nested_heading_list[] = array(
					'heading'  => $heading,
					'index'    => $index + $key,
					'children' => gutenberg_block_core_table_of_contents_linear_to_nested_heading_list(
						array_slice(
							$heading_list,
							$key + 1,
							$end_of_slice - ( $key + 1 )
						),
						$index + $key + 1
					),
				);
			} else {
				// No child node: Push a new node onto the return array.
				$nested_heading_list[] = array(
					'heading'  => $heading,
					'index'    => $index + $key,
					'children' => null,
				);
			}
		}
	}

	return $nested_heading_list;
}

/**
 * Renders the heading list of the `core/table-of-contents` block on server.
 *
 * @access private
 *
 * @param array  $nested_heading_list Nested list of heading data.
 * @param string $page_url            URL of the page the block belongs to.
 *
 * @return string The heading list rendered as HTML.
 */
function gutenberg_block_core_table_of_contents_render_list(
	$nested_heading_list,
	$page_url
) {
	$entry_class = 'wp-block-table-of-contents__entry';

	$child_nodes = array_map(
		function ( $child_node ) use ( $entry_class, $page_url ) {
			global $multipage;

			$id      = $child_node['heading']['id'];
			$content = $child_node['heading']['content'];

			if ( isset( $id ) ) {
				if ( $multipage ) {
					$href = add_query_arg(
						'page',
						(string) $child_node['heading']['page'],
						remove_query_arg( 'page', $page_url )
					) . '#' . $id;
				} else {
					$href = $page_url . '#' . $id;
				}

				$entry = sprintf(
					'<a class="%1$s" href="%2$s">%3$s</a>',
					esc_attr( $entry_class ),
					esc_url( $href ),
					esc_html( $content )
				);
			} else {
				$entry = sprintf(
					'<span class="%1$s">%2$s</span>',
					esc_attr( $entry_class ),
					esc_html( $content )
				);
			}

			return sprintf(
				'<li>%1$s%2$s</li>',
				$entry,
				$child_node['children']
					? gutenberg_block_core_table_of_contents_render_list(
						$child_node['children'],
						esc_url( $page_url )
					)
					: null
			);
		},
		$nested_heading_list
	);

	return '<ul>' . implode( $child_nodes ) . '</ul>';
}

/**
 * Renders the `core/table-of-contents` block on server.
 *
 * @access private
 *
 * @param  array    $attributes Block attributes.
 * @param  string   $content    Block default content.
 * @param  WP_Block $block      Block instance.
 *
 * @return string Rendered block HTML.
 */
function gutenberg_render_block_core_table_of_contents( $attributes, $content, $block ) {
	if ( ! isset( $block->context['postId'] ) ) {
		return '';
	}

	$headings = gutenberg_block_core_table_of_contents_get_headings(
		$block->context['postId'],
		$attributes['onlyIncludeCurrentPage']
	);

	// If there are no headings.
	if ( count( $headings ) === 0 ) {
		return '';
	}

	return sprintf(
		'<nav %1$s>%2$s</nav>',
		get_block_wrapper_attributes(),
		gutenberg_block_core_table_of_contents_render_list(
			gutenberg_block_core_table_of_contents_linear_to_nested_heading_list( $headings ),
			get_permalink( $block->context['postId'] )
		)
	);
}

/**
 * Registers the `core/table-of-contents` block on server.
 *
 * @access private
 *
 * @uses gutenberg_render_block_core_table_of_contents()
 *
 * @throws WP_Error An exception parsing the block definition.
 */
function gutenberg_register_block_core_table_of_contents() {
	register_block_type_from_metadata(
		__DIR__ . '/table-of-contents',
		array(
			'render_callback' => 'gutenberg_render_block_core_table_of_contents',
		)
	);
}
add_action( 'init', 'gutenberg_register_block_core_table_of_contents', 20 );