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 . '' ); 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 ) . '' ); } /** * 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(), '
    ' . htmlspecialchars( $alt, ENT_QUOTES, 'UTF-8' ) . '
    ' ); } /** * 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 ) . '}'; } }