contents to
$p = $doc->createElement( 'p' );
while ( $list->firstChild->firstChild ) {
// If contents is a block element, place outside the paragraph
// and start a new paragraph after
if ( CommentUtils::isBlockElement( $list->firstChild->firstChild ) ) {
if ( $p->firstChild ) {
$insertBefore = $referenceNode->nextSibling;
$referenceNode = $p;
$container->insertBefore( $p, $insertBefore );
}
$insertBefore = $referenceNode->nextSibling;
$referenceNode = $list->firstChild->firstChild;
$container->insertBefore( $list->firstChild->firstChild, $insertBefore );
$p = $doc->createElement( 'p' );
} else {
$p->appendChild( $list->firstChild->firstChild );
}
}
if ( $p->firstChild ) {
$insertBefore = $referenceNode->nextSibling;
$referenceNode = $p;
$container->insertBefore( $p, $insertBefore );
}
$list->removeChild( $list->firstChild );
} else {
// Text node / comment node, probably empty
$insertBefore = $referenceNode->nextSibling;
$referenceNode = $list->firstChild;
$container->insertBefore( $list->firstChild, $insertBefore );
}
}
$container->removeChild( $list );
}
/**
* Add another list item after the given one.
*
* @param Element $previousItem
* @return Element
*/
public static function addSiblingListItem( Element $previousItem ): Element {
$listItem = $previousItem->ownerDocument->createElement( $previousItem->tagName );
self::whitespaceParsoidHack( $listItem );
$previousItem->parentNode->insertBefore( $listItem, $previousItem->nextSibling );
return $listItem;
}
/**
* Create an element that will convert to the provided wikitext
*
* @param Document $doc
* @param string $wikitext
* @return Element
*/
public static function createWikitextNode( Document $doc, string $wikitext ): Element {
$span = $doc->createElement( 'span' );
$span->setAttribute( 'typeof', 'mw:Transclusion' );
$span->setAttribute( 'data-mw', json_encode( [ 'parts' => [ $wikitext ] ] ) );
return $span;
}
/**
* Check whether wikitext contains a user signature.
*
* @param string $wikitext
* @return bool
*/
public static function isWikitextSigned( string $wikitext ): bool {
$wikitext = CommentUtils::htmlTrim( $wikitext );
// Contains ~~~~ (four tildes), but not ~~~~~ (five tildes), at the end.
return (bool)preg_match( '/([^~]|^)~~~~$/', $wikitext );
}
/**
* Check whether HTML node contains a user signature.
*
* @param Element $container
* @return bool
*/
public static function isHtmlSigned( Element $container ): bool {
// Good enough?…
$matches = DOMCompat::querySelectorAll( $container, 'span[typeof="mw:Transclusion"][data-mw*="~~~~"]' );
// Iterate to get the last item. We don't know if $matches is an array or some iterator,
// and there doesn't seem to be a nicer way to get just the last item.
foreach ( $matches as $match ) {
$lastSig = $match;
}
if ( !isset( $lastSig ) ) {
// List was empty
return false;
}
// Signature must be at the end of the comment - there must be no sibling following this node, or its parents
$node = $lastSig;
while ( $node ) {
// Skip over whitespace nodes
while (
$node->nextSibling &&
$node->nextSibling->nodeType === XML_TEXT_NODE &&
CommentUtils::htmlTrim( $node->nextSibling->nodeValue ?? '' ) === ''
) {
$node = $node->nextSibling;
}
if ( $node->nextSibling ) {
return false;
}
$node = $node->parentNode;
}
return true;
}
/**
* Append a user signature to the comment in the container.
*
* @param Element $container
*/
public static function appendSignature( Element $container ): void {
$doc = $container->ownerDocument;
$signature = wfMessage( 'discussiontools-signature-prefix' )->inContentLanguage()->text() . '~~~~';
// If the last node isn't a paragraph (e.g. it's a list created in visual mode), then
// add another paragraph to contain the signature.
if ( !$container->lastChild || strtolower( $container->lastChild->nodeName ) !== 'p' ) {
$container->appendChild( $doc->createElement( 'p' ) );
}
// If the last node is empty, trim the signature to prevent leading whitespace triggering
// preformatted text (T269188, T276612)
if ( !$container->lastChild->firstChild ) {
$signature = ltrim( $signature, ' ' );
}
// Sign the last line
$container->lastChild->appendChild(
self::createWikitextNode(
$doc,
$signature
)
);
}
/**
* Add a reply to a specific comment
*
* @param ThreadItem $comment Comment being replied to
* @param Element $container Container of comment DOM nodes
*/
public static function addReply( ThreadItem $comment, Element $container ): void {
$newParsoidItem = null;
// Transfer comment DOM to Parsoid DOM
// Wrap every root node of the document in a new list item (dd/li).
// In wikitext mode every root node is a paragraph.
// In visual mode the editor takes care of preventing problematic nodes
// like
or from ever occurring in the comment.
while ( $container->childNodes->length ) {
if ( !$newParsoidItem ) {
$newParsoidItem = self::addListItem( $comment );
} else {
$newParsoidItem = self::addSiblingListItem( $newParsoidItem );
}
$newParsoidItem->appendChild( $container->firstChild );
}
}
/**
* Create a container of comment DOM nodes from wikitext
*
* @param CommentItem $comment Comment being replied to
* @param string $wikitext
*/
public static function addWikitextReply( CommentItem $comment, string $wikitext ): void {
$doc = $comment->getRange()->endContainer->ownerDocument;
$container = $doc->createElement( 'div' );
$wikitext = self::sanitizeWikitextLinebreaks( $wikitext );
$lines = explode( "\n", $wikitext );
foreach ( $lines as $line ) {
$p = $doc->createElement( 'p' );
$p->appendChild( self::createWikitextNode( $doc, $line ) );
$container->appendChild( $p );
}
if ( !self::isWikitextSigned( $wikitext ) ) {
self::appendSignature( $container );
}
self::addReply( $comment, $container );
}
/**
* Create a container of comment DOM nodes from HTML
*
* @param CommentItem $comment Comment being replied to
* @param string $html
*/
public static function addHtmlReply( CommentItem $comment, string $html ): void {
$doc = $comment->getRange()->endContainer->ownerDocument;
$container = $doc->createElement( 'div' );
DOMCompat::setInnerHTML( $container, $html );
// Remove empty lines
// This should really be anything that serializes to empty string in wikitext,
// (e.g. ) but this will catch most cases
// Create a non-live child node list, so we don't have to worry about it changing
// as nodes are removed.
$childNodeList = iterator_to_array( $container->childNodes );
foreach ( $childNodeList as $node ) {
if (
strtolower( $node->nodeName ) === 'p' &&
CommentUtils::htmlTrim( DOMCompat::getInnerHTML( $node ) ) === ''
) {
$container->removeChild( $node );
}
}
if ( !self::isHtmlSigned( $container ) ) {
self::appendSignature( $container );
}
self::addReply( $comment, $container );
}
}