loadHTML( '
…hook into WordPress at the
plugins_loadedaction…
`, * which renders as one sentence broken across three lines (and, before this, * shipped the bare tag through the custom-HTML fallback). The repair follows * the sentence: a loose run continues the previous paragraph when that * paragraph is plainly unfinished (no terminal punctuation), and pulls the * following paragraph in when IT plainly continues the sentence (starts * lowercase). A run that is NOT mid-sentence just gains its own paragraph — * the same rendering the browser's anonymous block gave it, but as a real, * editable block. Mutates the parsed DOM in place, before block mapping. * * @param \DOMElement $root The parsed content's wrapping element. */ private static function heal_loose_inlines( \DOMElement $root ) { $doc = $root->ownerDocument; $nodes = array(); foreach ( $root->childNodes as $node ) { $nodes[] = $node; // Snapshot: the loop moves nodes around. } $open = null; // The paragraph currently absorbing a mid-sentence run. $last = null; // The previous significant top-level node. foreach ( $nodes as $node ) { if ( XML_TEXT_NODE === $node->nodeType && '' === trim( $node->textContent ) ) { continue; // Formatting whitespace between blocks. } $tag = XML_ELEMENT_NODE === $node->nodeType ? strtolower( $node->nodeName ) : ''; $loose = XML_TEXT_NODE === $node->nodeType || in_array( $tag, self::INLINE_TAGS, true ); if ( $loose ) { if ( ! $open ) { if ( $last instanceof \DOMElement && 'p' === strtolower( $last->nodeName ) && self::sentence_open( $last->textContent ) ) { $open = $last; // The run continues the unfinished sentence. } else { $open = $doc->createElement( 'p' ); // Standalone run: its own paragraph. $root->insertBefore( $open, $node ); $last = $open; } } if ( $open->hasChildNodes() ) { $open->appendChild( $doc->createTextNode( ' ' ) ); // The seam the source line break stood for. } $open->appendChild( $node ); continue; } if ( $open && 'p' === $tag && self::sentence_open( $open->textContent ) && preg_match( '/^\p{Ll}/u', trim( $node->textContent ) ) ) { // The sentence runs on into this paragraph — fold it in. $open->appendChild( $doc->createTextNode( ' ' ) ); while ( $node->firstChild ) { $open->appendChild( $node->firstChild ); } $root->removeChild( $node ); $open = null; continue; } $open = null; $last = $node; } } /** * PURE: whether a paragraph's text ends mid-sentence — no terminal * punctuation (closing quotes/brackets may trail it). Colons and * semicolons count as terminal: "the following:" introduces a block, * it doesn't continue into one. * * @param string $text The paragraph's plain text. * @return bool */ private static function sentence_open( $text ) { $text = trim( (string) $text ); return '' !== $text && ! preg_match( '/[.!?…:;]["\'\x{201D}\x{2019})\]]*$/u', $text ); } /** * One top-level DOM node → one serialised core block. * * @param \DOMNode $node A child of the parsed content root. * @return string Block markup, or '' for ignorable nodes. */ private static function node_to_block( \DOMNode $node ) { if ( XML_TEXT_NODE === $node->nodeType ) { $text = trim( $node->textContent ); return '' === $text ? '' : "\n" . esc_html( $text ) . "
\n"; } if ( XML_ELEMENT_NODE !== $node->nodeType ) { return ''; } $tag = strtolower( $node->nodeName ); switch ( $tag ) { case 'p': return "\n" . self::dom_inner_html( $node ) . "
\n"; case 'h2': return "\n" . trim( $loose ) . "
\n"; $loose = ''; } $inner .= "\n" . self::dom_inner_html( $child ) . "
\n"; } else { $loose .= $node->ownerDocument->saveHTML( $child ); } } if ( '' !== trim( $loose ) ) { $inner .= "\n" . trim( $loose ) . "
\n"; } return "\n" . $inner . "\n"; case 'figure': // Our injected image figure — recover the attachment id from the // wp-image-N class the editor itself uses. $img = $node->getElementsByTagName( 'img' )->item( 0 ); $id = 0; if ( $img && preg_match( '/wp-image-(\d+)/', (string) $img->getAttribute( 'class' ), $m ) ) { $id = (int) $m[1]; } $attrs = $id > 0 ? sprintf( ' {"id":%d,"sizeSlug":"large","linkDestination":"none"}', $id ) : ''; return '\n" . $node->ownerDocument->saveHTML( $node ) . "\n"; default: // Outside the contract's vocabulary: render verbatim through the // custom-HTML block — displays identically, still valid blocks. return "\n" . $node->ownerDocument->saveHTML( $node ) . "\n"; } } /** * The inner HTML of a DOM node, serialised by its own document. * * @param \DOMNode $node Node. * @return string */ private static function dom_inner_html( \DOMNode $node ) { $html = ''; foreach ( $node->childNodes as $child ) { $html .= $node->ownerDocument->saveHTML( $child ); } return $html; } }