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 );
$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 if an element created by ::createWikitextNode() starts with list item markup.
*
* @param Element $node
* @return bool
*/
private static function isWikitextNodeListItem( Element $node ): bool {
$dataMw = json_decode( $node->getAttribute( 'data-mw' ) ?? '', true );
$wikitextLine = $dataMw['parts'][0] ?? null;
return $wikitextLine && is_string( $wikitextLine ) &&
in_array( $wikitextLine[0], [ '*', '#', ':', ';' ] );
}
/**
* Append a user signature to the comment in the container.
*
* @param DocumentFragment $container
* @param string $signature
*/
public static function appendSignature( DocumentFragment $container, string $signature ): void {
$doc = $container->ownerDocument;
// If the last node isn't a paragraph (e.g. it's a list created in visual mode),
// or looks like a list item created in wikitext mode (T263217),
// then add another paragraph to contain the signature.
$wrapperNode = $container->lastChild;
if (
!( $wrapperNode instanceof Element ) ||
strtolower( $wrapperNode->tagName ) !== 'p' ||
(
// This would be easier to check in prepareWikitextReply(), but that would result
// in an empty list item being added at the end if we don't need to add a signature.
( $wtNode = $wrapperNode->lastChild ) &&
$wtNode instanceof Element &&
static::isWikitextNodeListItem( $wtNode )
)
) {
$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(
static::createWikitextNode(
$doc,
$signature
)
);
}
/**
* Append a user signature to the comment in the provided wikitext.
*
* @param string $wikitext
* @param string $signature
* @return string
*/
public static function appendSignatureWikitext( string $wikitext, string $signature ): string {
$wikitext = CommentUtils::htmlTrim( $wikitext );
$lines = explode( "\n", $wikitext );
$lastLine = end( $lines );
// If last line looks like a list item, add an empty line afterwards for the signature (T263217)
if ( $lastLine && in_array( $lastLine[0], [ '*', '#', ':', ';' ] ) ) {
$wikitext .= "\n";
// Trim the signature to prevent leading whitespace triggering preformatted text (T269188, T276612)
$signature = ltrim( $signature, ' ' );
}
return $wikitext . $signature;
}
/**
* Add a reply to a specific comment
*
* @param ContentThreadItem $comment Comment being replied to
* @param DocumentFragment $container Container of comment DOM nodes
*/
public static function addReply( ContentThreadItem $comment, DocumentFragment $container ): void {
$services = MediaWikiServices::getInstance();
$dtConfig = $services->getConfigFactory()->makeConfig( 'discussiontools' );
$replyIndentation = $dtConfig->get( 'DiscussionToolsReplyIndentation' );
$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 = static::addListItem( $comment, $replyIndentation );
} else {
$newParsoidItem = static::addSiblingListItem( $newParsoidItem );
}
// Suppress space after the indentation character to support nested lists (T238218).
// By request from the community, avoid this if possible after bullet indentation (T259864).
if ( !(
$replyIndentation === 'bullet' &&
( $wtNode = $container->firstChild->lastChild ) &&
$wtNode instanceof Element &&
!static::isWikitextNodeListItem( $wtNode )
) ) {
static::whitespaceParsoidHack( $newParsoidItem );
}
$newParsoidItem->appendChild( $container->firstChild );
}
}
/**
* Transfer comment DOM nodes into a list node, as if adding a reply, but without requiring a
* ThreadItem.
*
* @param DocumentFragment $container Container of comment DOM nodes
* @return Element $node List node
*/
public static function transferReply( DocumentFragment $container ): Element {
$services = MediaWikiServices::getInstance();
$dtConfig = $services->getConfigFactory()->makeConfig( 'discussiontools' );
$replyIndentation = $dtConfig->get( 'DiscussionToolsReplyIndentation' );
$doc = $container->ownerDocument;
// Like addReply(), but we make our own list
$list = $doc->createElement( $replyIndentation === 'invisible' ? 'dl' : 'ul' );
while ( $container->childNodes->length ) {
$item = $doc->createElement( $replyIndentation === 'invisible' ? 'dd' : 'li' );
// Suppress space after the indentation character to support nested lists (T238218).
// By request from the community, avoid this if possible after bullet indentation (T259864).
if ( !(
$replyIndentation === 'bullet' &&
( $wtNode = $container->firstChild->lastChild ) &&
$wtNode instanceof Element &&
!static::isWikitextNodeListItem( $wtNode )
) ) {
static::whitespaceParsoidHack( $item );
}
$item->appendChild( $container->firstChild );
$list->appendChild( $item );
}
return $list;
}
/**
* Create a container of comment DOM nodes from wikitext
*
* @param Document $doc Document where the DOM nodes will be inserted
* @param string $wikitext
* @return DocumentFragment DOM nodes
*/
public static function prepareWikitextReply( Document $doc, string $wikitext ): DocumentFragment {
$container = $doc->createDocumentFragment();
$wikitext = static::sanitizeWikitextLinebreaks( $wikitext );
$lines = explode( "\n", $wikitext );
foreach ( $lines as $line ) {
$p = $doc->createElement( 'p' );
$p->appendChild( static::createWikitextNode( $doc, $line ) );
$container->appendChild( $p );
}
return $container;
}
/**
* Create a container of comment DOM nodes from HTML
*
* @param Document $doc Document where the DOM nodes will be inserted
* @param string $html
* @return DocumentFragment DOM nodes
*/
public static function prepareHtmlReply( Document $doc, string $html ): DocumentFragment {
$container = DOMUtils::parseHTMLToFragment( $doc, $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 ( (
$node instanceof Text &&
CommentUtils::htmlTrim( $node->nodeValue ?? '' ) === ''
) || (
$node instanceof Element &&
strtolower( $node->tagName ) === 'p' &&
CommentUtils::htmlTrim( DOMCompat::getInnerHTML( $node ) ) === ''
) ) {
$container->removeChild( $node );
}
}
return $container;
}
/**
* Add a reply in the DOM to a comment using wikitext.
*
* @param ContentCommentItem $comment Comment being replied to
* @param string $wikitext
* @param string|null $signature
*/
public static function addWikitextReply(
ContentCommentItem $comment, string $wikitext, string $signature = null
): void {
$doc = $comment->getRange()->endContainer->ownerDocument;
$container = static::prepareWikitextReply( $doc, $wikitext );
if ( $signature !== null ) {
static::appendSignature( $container, $signature );
}
static::addReply( $comment, $container );
}
/**
* Add a reply in the DOM to a comment using HTML.
*
* @param ContentCommentItem $comment Comment being replied to
* @param string $html
* @param string|null $signature
*/
public static function addHtmlReply(
ContentCommentItem $comment, string $html, string $signature = null
): void {
$doc = $comment->getRange()->endContainer->ownerDocument;
$container = static::prepareHtmlReply( $doc, $html );
if ( $signature !== null ) {
static::appendSignature( $container, $signature );
}
static::addReply( $comment, $container );
}
}