>
*/
private static function blocks_from_element( \DOMElement $element ): array {
$tag = strtolower( $element->tagName );
if ( in_array( $tag, array( 'div', 'span', 'section', 'article', 'main', 'body' ), true ) ) {
$inner = self::blocks_from_children( $element );
if ( ! empty( $inner ) ) {
return $inner;
}
$text = trim( (string) $element->textContent );
if ( '' === $text ) {
return array();
}
return array( self::paragraph_block( self::inner_html( $element ) ) );
}
if ( preg_match( '/^h([1-6])$/', $tag, $m ) ) {
return array( self::heading_block( (int) $m[1], self::inner_html( $element ) ) );
}
if ( 'p' === $tag ) {
if ( self::is_empty_element( $element ) ) {
return array();
}
if ( self::element_is_image_marker_only( $element ) ) {
return array( self::passthrough_html( '' . self::inner_html( $element ) . '
' ) );
}
if ( self::element_is_code_only( $element ) ) {
return array( self::code_block( (string) $element->textContent ) );
}
return array( self::paragraph_block( self::inner_html( $element ) ) );
}
if ( 'pre' === $tag ) {
return array( self::code_block( self::code_text_from_pre( $element ) ) );
}
if ( 'ul' === $tag || 'ol' === $tag ) {
return array( self::list_block( $element ) );
}
if ( 'blockquote' === $tag ) {
return array( self::quote_block( $element ) );
}
if ( 'hr' === $tag ) {
return array( self::separator_block() );
}
if ( 'table' === $tag ) {
$block = self::table_block( $element );
return empty( $block ) ? array() : array( $block );
}
if ( 'figure' === $tag ) {
$table = self::first_child_table( $element );
if ( $table ) {
$block = self::table_block( $table );
return empty( $block ) ? array() : array( $block );
}
return self::blocks_from_children( $element );
}
if ( self::is_empty_element( $element ) ) {
return array();
}
return array( self::paragraph_block( self::inner_html( $element ) ) );
}
/**
* @return array
*/
private static function heading_block( int $level, string $inner ): array {
$level = max( 1, min( 6, $level ) );
$inner = wp_kses_post( $inner );
$html = '' . $inner . '';
$attrs = 2 === $level ? array() : array( 'level' => $level );
return self::make_block( 'core/heading', $attrs, $html );
}
/**
* @return array
*/
private static function paragraph_block( string $inner ): array {
$inner = wp_kses_post( trim( $inner ) );
return self::make_block( 'core/paragraph', array(), '' . $inner . '
' );
}
/**
* Leave raw HTML for a later recipe (e.g. [image:] → core/image).
*
* @return array
*/
private static function passthrough_html( string $html ): array {
return array(
'blockName' => null,
'attrs' => array(),
'innerBlocks' => array(),
'innerHTML' => $html,
'innerContent' => array( $html ),
);
}
/**
* @return array
*/
private static function code_block( string $text ): array {
$text = html_entity_decode( $text, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
$text = str_replace( "\r\n", "\n", $text );
$text = rtrim( str_replace( "\r", "\n", $text ), "\n" );
$html = '' . esc_html( $text ) . '
';
return self::make_block( 'core/code', array(), $html );
}
/**
* @return array
*/
private static function list_block( \DOMElement $list ): array {
$ordered = 'ol' === strtolower( $list->tagName );
$tag = $ordered ? 'ol' : 'ul';
$attrs = $ordered ? array( 'ordered' => true ) : array();
$items = array();
foreach ( $list->childNodes as $child ) {
if ( ! $child instanceof \DOMElement ) {
continue;
}
$child_tag = strtolower( $child->tagName );
if ( 'li' === $child_tag ) {
$items[] = self::list_item_block( $child );
}
}
$open = '<' . $tag . ' class="wp-block-list">';
$close = '' . $tag . '>';
$inner_content = array( $open );
$inner_html = $open;
foreach ( $items as $index => $item ) {
if ( $index > 0 ) {
$inner_content[] = '';
}
$inner_content[] = null;
$inner_html .= $item['innerHTML'];
}
$inner_content[] = $close;
$inner_html .= $close;
return self::make_block( 'core/list', $attrs, $inner_html, $items, $inner_content );
}
/**
* @return array
*/
private static function list_item_block( \DOMElement $li ): array {
$nested = array();
$parts = array();
foreach ( $li->childNodes as $child ) {
if ( $child instanceof \DOMElement && in_array( strtolower( $child->tagName ), array( 'ul', 'ol' ), true ) ) {
$nested[] = self::list_block( $child );
continue;
}
$parts[] = $li->ownerDocument ? $li->ownerDocument->saveHTML( $child ) : '';
}
$inner = wp_kses_post( implode( '', $parts ) );
if ( empty( $nested ) ) {
return self::make_block( 'core/list-item', array(), '' . $inner . '' );
}
$inner_content = array( '' . $inner );
foreach ( $nested as $index => $block ) {
if ( $index > 0 ) {
$inner_content[] = '';
}
$inner_content[] = null;
}
$inner_content[] = '';
return self::make_block( 'core/list-item', array(), '' . $inner . '', $nested, $inner_content );
}
/**
* @return array
*/
private static function quote_block( \DOMElement $quote ): array {
$inner_blocks = self::blocks_from_children( $quote );
if ( empty( $inner_blocks ) ) {
$text = trim( (string) $quote->textContent );
if ( '' !== $text ) {
$inner_blocks[] = self::paragraph_block( esc_html( $text ) );
}
}
$open = '';
$close = '
';
$inner_content = array( $open );
$inner_html = $open;
foreach ( $inner_blocks as $index => $block ) {
if ( $index > 0 ) {
$inner_content[] = '';
}
$inner_content[] = null;
$inner_html .= $block['innerHTML'];
}
$inner_content[] = $close;
$inner_html .= $close;
return self::make_block( 'core/quote', array(), $inner_html, $inner_blocks, $inner_content );
}
/**
* @return array
*/
private static function separator_block(): array {
return self::make_block(
'core/separator',
array(),
'
'
);
}
/**
* Markdown / HTML tables → core/table (figure + thead/tbody).
*
* @return array
*/
private static function table_block( \DOMElement $table ): array {
$html = self::serialize_core_table_html( $table );
if ( '' === $html ) {
return array();
}
return self::make_block( 'core/table', array(), $html );
}
/**
* @return \DOMElement|null
*/
private static function first_child_table( \DOMElement $parent ) {
foreach ( $parent->childNodes as $child ) {
if ( $child instanceof \DOMElement && 'table' === strtolower( $child->tagName ) ) {
return $child;
}
}
return null;
}
/**
* Markup Gutenberg expects for core/table save().
*/
private static function serialize_core_table_html( \DOMElement $table ): string {
$head = '';
$body = '';
$foot = '';
$loose = array();
foreach ( $table->childNodes as $child ) {
if ( ! $child instanceof \DOMElement ) {
continue;
}
$tag = strtolower( $child->tagName );
if ( 'thead' === $tag ) {
$head .= self::serialize_table_section( $child, true );
} elseif ( 'tbody' === $tag ) {
$body .= self::serialize_table_section( $child, false );
} elseif ( 'tfoot' === $tag ) {
$foot .= self::serialize_table_section( $child, false );
} elseif ( 'tr' === $tag ) {
$loose[] = $child;
}
}
if ( '' === $head && '' === $body && ! empty( $loose ) ) {
$first = array_shift( $loose );
if ( $first instanceof \DOMElement && self::row_is_header( $first ) ) {
$head = '' . self::serialize_table_row( $first, true ) . '';
} elseif ( $first instanceof \DOMElement ) {
array_unshift( $loose, $first );
}
if ( ! empty( $loose ) ) {
$body = '';
foreach ( $loose as $row ) {
if ( $row instanceof \DOMElement ) {
$body .= self::serialize_table_row( $row, false );
}
}
$body .= '';
}
}
if ( '' === $head && '' === $body && '' === $foot ) {
return '';
}
return '' . $head . $body . $foot . '
';
}
private static function serialize_table_section( \DOMElement $section, bool $header ): string {
$tag = strtolower( $section->tagName );
$rows = '';
foreach ( $section->childNodes as $child ) {
if ( $child instanceof \DOMElement && 'tr' === strtolower( $child->tagName ) ) {
$rows .= self::serialize_table_row( $child, $header );
}
}
if ( '' === $rows ) {
return '';
}
return '<' . $tag . '>' . $rows . '' . $tag . '>';
}
private static function serialize_table_row( \DOMElement $row, bool $header ): string {
$cells = '';
foreach ( $row->childNodes as $child ) {
if ( ! $child instanceof \DOMElement ) {
continue;
}
$tag = strtolower( $child->tagName );
if ( ! in_array( $tag, array( 'td', 'th' ), true ) ) {
continue;
}
$out_tag = $header ? 'th' : $tag;
$inner = wp_kses_post( self::inner_html( $child ) );
$cells .= '<' . $out_tag . '>' . $inner . '' . $out_tag . '>';
}
if ( '' === $cells ) {
return '';
}
return '' . $cells . '
';
}
private static function row_is_header( \DOMElement $row ): bool {
$th = 0;
$td = 0;
foreach ( $row->childNodes as $child ) {
if ( ! $child instanceof \DOMElement ) {
continue;
}
$tag = strtolower( $child->tagName );
if ( 'th' === $tag ) {
++$th;
} elseif ( 'td' === $tag ) {
++$td;
}
}
return $th > 0 && 0 === $td;
}
/**
* @param array $attrs Attributes.
* @param array> $inner_blocks Nested blocks.
* @param array|null $inner_content Chunks.
* @return array
*/
private static function make_block( string $name, array $attrs, string $inner_html, array $inner_blocks = array(), $inner_content = null ): array {
if ( null === $inner_content ) {
$inner_content = array( $inner_html );
}
return array(
'blockName' => $name,
'attrs' => $attrs,
'innerBlocks' => $inner_blocks,
'innerHTML' => $inner_html,
'innerContent' => $inner_content,
);
}
private static function inner_html( \DOMElement $element ): string {
$dom = $element->ownerDocument;
if ( ! $dom ) {
return '';
}
$html = '';
foreach ( $element->childNodes as $child ) {
$html .= $dom->saveHTML( $child );
}
return trim( $html );
}
private static function code_text_from_pre( \DOMElement $pre ): string {
$code = $pre->getElementsByTagName( 'code' )->item( 0 );
if ( $code instanceof \DOMElement ) {
return (string) $code->textContent;
}
return (string) $pre->textContent;
}
private static function element_is_image_marker_only( \DOMElement $element ): bool {
$text = trim( (string) $element->textContent );
return (bool) preg_match( '/^\[image:\s*[^\]]+\]$/iu', $text );
}
private static function element_is_code_only( \DOMElement $element ): bool {
$code_nodes = 0;
$other_text = '';
foreach ( $element->childNodes as $child ) {
if ( $child instanceof \DOMText ) {
$other_text .= $child->textContent;
continue;
}
if ( ! $child instanceof \DOMElement ) {
continue;
}
if ( 'code' === strtolower( $child->tagName ) ) {
++$code_nodes;
continue;
}
$other_text .= $child->textContent;
}
return $code_nodes > 0 && '' === trim( $other_text );
}
private static function is_empty_element( \DOMElement $element ): bool {
$text = trim( (string) $element->textContent );
if ( '' !== $text ) {
return false;
}
return 0 === $element->getElementsByTagName( 'img' )->length;
}
/**
* @return \DOMDocument|null
*/
private static function load_html_dom( string $html ) {
$wrapped = '' . $html . '
';
$dom = new \DOMDocument();
$prev = libxml_use_internal_errors( true );
$loaded = $dom->loadHTML(
'' . $wrapped,
LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
);
libxml_clear_errors();
libxml_use_internal_errors( $prev );
return $loaded ? $dom : null;
}
/**
* @return \DOMElement|null
*/
private static function resolve_root( \DOMDocument $dom ) {
$xpath = new \DOMXPath( $dom );
$nodes = $xpath->query( '//*[@data-forwp-drive="blocks"]' );
if ( $nodes && $nodes->length > 0 && $nodes->item( 0 ) instanceof \DOMElement ) {
return $nodes->item( 0 );
}
$body = $dom->getElementsByTagName( 'body' )->item( 0 );
return $body instanceof \DOMElement ? $body : null;
}
}