loadHTML(
'
' . $html . '
',
LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
);
libxml_clear_errors();
libxml_use_internal_errors( $prev );
$xpath = new DOMXPath( $dom );
$root = $xpath->query( '//div[@id="draftdrop-root"]' )->item( 0 );
if ( ! $root ) {
return self::block( 'html', array(), $html );
}
return implode( "\n\n", self::children( $root ) );
}
/**
* Convert all child nodes of a node into blocks.
*
* @param DOMNode $container Parent node.
* @return string[]
*/
private static function children( DOMNode $container ) {
$blocks = array();
foreach ( $container->childNodes as $node ) {
$block = self::node( $node );
if ( '' !== $block ) {
$blocks[] = $block;
}
}
return $blocks;
}
/**
* Convert one node into a block.
*
* @param DOMNode $n Node.
* @return string
*/
private static function node( DOMNode $n ) {
if ( XML_TEXT_NODE === $n->nodeType || XML_CDATA_SECTION_NODE === $n->nodeType ) {
$text = trim( $n->textContent );
if ( '' === $text ) {
return '';
}
return self::block( 'paragraph', array(), '' . htmlspecialchars( $text, ENT_NOQUOTES, 'UTF-8' ) . '
' );
}
if ( XML_ELEMENT_NODE !== $n->nodeType ) {
return ''; // Comments and processing instructions are dropped.
}
$tag = strtolower( $n->nodeName );
switch ( $tag ) {
case 'p':
$img = self::sole_image( $n );
if ( $img ) {
return self::image_block( $img );
}
$inner = trim( self::inner( $n ) );
if ( '' === $inner ) {
return '';
}
return self::block( 'paragraph', array(), '' . $inner . '
' );
case 'h1':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6':
$level = (int) substr( $tag, 1 );
$inner = trim( self::inner( $n ) );
if ( '' === $inner ) {
return '';
}
$attrs = 2 === $level ? array() : array( 'level' => $level );
return self::block( 'heading', $attrs, '<' . $tag . ' class="wp-block-heading">' . $inner . '' . $tag . '>' );
case 'ul':
case 'ol':
return self::list_block( $n );
case 'blockquote':
$inner = implode( "\n\n", self::children( $n ) );
return self::block( 'quote', array(), '' . "\n" . $inner . "\n" . '
' );
case 'pre':
$code = htmlspecialchars( rtrim( $n->textContent, "\n" ), ENT_NOQUOTES, 'UTF-8' );
return self::block( 'code', array(), '' . $code . '
' );
case 'hr':
return self::block( 'separator', array(), '
' );
case 'table':
self::strip_presentation_attributes( $n );
if ( $n instanceof DOMElement ) {
$n->setAttribute( 'class', 'has-fixed-layout' ); // The table block's default layout, as Gutenberg itself saves it.
}
return self::block( 'table', array(), '' . $n->ownerDocument->saveHTML( $n ) . '' );
case 'img':
return self::image_block( $n );
default:
return self::block( 'html', array(), $n->ownerDocument->saveHTML( $n ) );
}
}
/**
* Build a list block (with nested lists).
*
* @param DOMNode $el ul or ol element.
* @return string
*/
private static function list_block( DOMNode $el ) {
$tag = strtolower( $el->nodeName );
$items = array();
foreach ( $el->childNodes as $li ) {
if ( XML_ELEMENT_NODE !== $li->nodeType || 'li' !== strtolower( $li->nodeName ) ) {
continue;
}
$text = '';
$nested = '';
foreach ( $li->childNodes as $child ) {
if ( XML_ELEMENT_NODE === $child->nodeType && in_array( strtolower( $child->nodeName ), array( 'ul', 'ol' ), true ) ) {
$nested .= "\n" . self::list_block( $child );
} else {
$text .= $li->ownerDocument->saveHTML( $child );
}
}
// Loose Markdown lists wrap items in ; list items hold inline content.
$text = preg_replace( '#
\s*#i', '
', $text );
$text = preg_replace( '#^\s*
|
\s*$#i', '', $text );
$items[] = self::block( 'list-item', array(), '' . trim( $text ) . $nested . '' );
}
if ( empty( $items ) ) {
return '';
}
$attrs = 'ol' === $tag ? array( 'ordered' => true ) : array();
return self::block(
'list',
$attrs,
'<' . $tag . ' class="wp-block-list">' . implode( "\n\n", $items ) . '' . $tag . '>'
);
}
/**
* Image block markup.
*
* @param DOMNode $img img element.
* @return string
*/
private static function image_block( DOMNode $img ) {
if ( ! ( $img instanceof DOMElement ) ) {
return '';
}
$src = $img->getAttribute( 'src' );
if ( '' === $src ) {
return '';
}
$alt = $img->getAttribute( 'alt' );
return self::block(
'image',
array(),
'
'
);
}
/**
* Return the img element if a paragraph contains nothing but one image.
*
* @param DOMNode $p Paragraph.
* @return DOMNode|null
*/
private static function sole_image( DOMNode $p ) {
$found = null;
foreach ( $p->childNodes as $c ) {
if ( XML_TEXT_NODE === $c->nodeType && '' === trim( $c->textContent ) ) {
continue;
}
if ( XML_ELEMENT_NODE === $c->nodeType && 'img' === strtolower( $c->nodeName ) && null === $found ) {
$found = $c;
continue;
}
return null;
}
return $found;
}
/**
* Remove inline styling that the table block would not round-trip.
*
* @param DOMNode $table Table element.
* @return void
*/
private static function strip_presentation_attributes( DOMNode $table ) {
if ( ! ( $table instanceof DOMElement ) ) {
return;
}
$elements = array( $table );
foreach ( $table->getElementsByTagName( '*' ) as $el ) {
$elements[] = $el;
}
foreach ( $elements as $el ) {
$el->removeAttribute( 'style' );
$el->removeAttribute( 'align' );
}
}
/**
* Inner HTML of a node.
*
* @param DOMNode $node Node.
* @return string
*/
private static function inner( DOMNode $node ) {
$html = '';
foreach ( $node->childNodes as $child ) {
$html .= $node->ownerDocument->saveHTML( $child );
}
return $html;
}
/**
* Wrap inner HTML in block delimiters.
*
* @param string $name Block name without the "core/" prefix.
* @param array $attrs Block attributes.
* @param string $inner Block HTML.
* @return string
*/
private static function block( $name, $attrs, $inner ) {
$json = empty( $attrs ) ? '' : ' ' . self::attrs_json( $attrs );
return '' . "\n" . $inner . "\n" . '';
}
/**
* Encode block attributes. Only integers and booleans are ever used, so a
* tiny encoder is enough and no string escaping is needed.
*
* @param array $attrs Attributes (key => int|bool).
* @return string
*/
private static function attrs_json( $attrs ) {
$pairs = array();
foreach ( $attrs as $key => $value ) {
if ( is_bool( $value ) ) {
$value = $value ? 'true' : 'false';
} else {
$value = (string) (int) $value;
}
$pairs[] = '"' . preg_replace( '/[^a-zA-Z0-9]/', '', (string) $key ) . '":' . $value;
}
return '{' . implode( ',', $pairs ) . '}';
}
}