mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/DiscussionTools
synced 2024-11-27 17:51:09 +00:00
Merge "Separate ContentThreadItem and DatabaseThreadItem etc."
This commit is contained in:
commit
d15a8c931a
|
@ -7,6 +7,7 @@ use ApiMain;
|
|||
use DerivativeContext;
|
||||
use DerivativeRequest;
|
||||
use MediaWiki\Extension\DiscussionTools\Hooks\HookUtils;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentCommentItem;
|
||||
use MediaWiki\Extension\VisualEditor\ApiParsoidTrait;
|
||||
use MediaWiki\Logger\LoggerFactory;
|
||||
use MediaWiki\MediaWikiServices;
|
||||
|
@ -232,7 +233,7 @@ class ApiDiscussionToolsEdit extends ApiBase {
|
|||
if ( $commentId ) {
|
||||
$comment = $threadItemSet->findCommentById( $commentId );
|
||||
|
||||
if ( !$comment || !( $comment instanceof CommentItem ) ) {
|
||||
if ( !$comment || !( $comment instanceof ContentCommentItem ) ) {
|
||||
$this->dieWithError( [ 'apierror-discussiontools-commentid-notfound', $commentId ] );
|
||||
}
|
||||
|
||||
|
@ -242,7 +243,7 @@ class ApiDiscussionToolsEdit extends ApiBase {
|
|||
|
||||
if ( count( $comments ) > 1 ) {
|
||||
$this->dieWithError( [ 'apierror-discussiontools-commentname-ambiguous', $commentName ] );
|
||||
} elseif ( !$comment || !( $comment instanceof CommentItem ) ) {
|
||||
} elseif ( !$comment || !( $comment instanceof ContentCommentItem ) ) {
|
||||
$this->dieWithError( [ 'apierror-discussiontools-commentname-notfound', $commentName ] );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ namespace MediaWiki\Extension\DiscussionTools;
|
|||
|
||||
use ApiBase;
|
||||
use ApiMain;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentHeadingItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentThreadItem;
|
||||
use MediaWiki\Extension\VisualEditor\ApiParsoidTrait;
|
||||
use Title;
|
||||
use Wikimedia\ParamValidator\ParamValidator;
|
||||
|
@ -50,12 +52,12 @@ class ApiDiscussionToolsPageInfo extends ApiBase {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get transcluded=from data for a ThreadItemSet
|
||||
* Get transcluded=from data for a ContentThreadItemSet
|
||||
*
|
||||
* @param ThreadItemSet $threadItemSet
|
||||
* @param ContentThreadItemSet $threadItemSet
|
||||
* @return array
|
||||
*/
|
||||
private static function getTranscludedFrom( ThreadItemSet $threadItemSet ): array {
|
||||
private static function getTranscludedFrom( ContentThreadItemSet $threadItemSet ): array {
|
||||
$threadItems = $threadItemSet->getThreadItems();
|
||||
$transcludedFrom = [];
|
||||
foreach ( $threadItems as $threadItem ) {
|
||||
|
@ -84,33 +86,33 @@ class ApiDiscussionToolsPageInfo extends ApiBase {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get thread items HTML for a ThreadItemSet
|
||||
* Get thread items HTML for a ContentThreadItemSet
|
||||
*
|
||||
* @param ThreadItemSet $threadItemSet
|
||||
* @param ContentThreadItemSet $threadItemSet
|
||||
* @return array
|
||||
*/
|
||||
private static function getThreadItemsHtml( ThreadItemSet $threadItemSet ): array {
|
||||
private static function getThreadItemsHtml( ContentThreadItemSet $threadItemSet ): array {
|
||||
$threads = $threadItemSet->getThreads();
|
||||
if ( count( $threads ) > 0 ) {
|
||||
$firstHeading = $threads[0];
|
||||
if ( !$firstHeading->isPlaceholderHeading() ) {
|
||||
$range = new ImmutableRange( $firstHeading->getRootNode(), 0, $firstHeading->getRootNode(), 0 );
|
||||
$fakeHeading = new HeadingItem( $range, null );
|
||||
$fakeHeading = new ContentHeadingItem( $range, null );
|
||||
$fakeHeading->setRootNode( $firstHeading->getRootNode() );
|
||||
$fakeHeading->setName( 'h-' );
|
||||
$fakeHeading->setId( 'h-' );
|
||||
array_unshift( $threads, $fakeHeading );
|
||||
}
|
||||
}
|
||||
$output = array_map( static function ( ThreadItem $item ) {
|
||||
return $item->jsonSerialize( true, static function ( array &$array, ThreadItem $item ) {
|
||||
$output = array_map( static function ( ContentThreadItem $item ) {
|
||||
return $item->jsonSerialize( true, static function ( array &$array, ContentThreadItem $item ) {
|
||||
$array['html'] = $item->getHtml();
|
||||
} );
|
||||
}, $threads );
|
||||
foreach ( $threads as $index => $item ) {
|
||||
// need to loop over this to fix up empty sections, because we
|
||||
// need context that's not available inside the array map
|
||||
if ( $item instanceof HeadingItem && count( $item->getReplies() ) === 0 ) {
|
||||
if ( $item instanceof ContentHeadingItem && count( $item->getReplies() ) === 0 ) {
|
||||
$nextItem = $threads[ $index + 1 ] ?? false;
|
||||
$startRange = $item->getRange();
|
||||
if ( $nextItem ) {
|
||||
|
|
|
@ -20,9 +20,9 @@ use Wikimedia\Parsoid\Utils\DOMUtils;
|
|||
trait ApiDiscussionToolsTrait {
|
||||
/**
|
||||
* @param RevisionRecord $revision
|
||||
* @return ThreadItemSet
|
||||
* @return ContentThreadItemSet
|
||||
*/
|
||||
protected function parseRevision( RevisionRecord $revision ): ThreadItemSet {
|
||||
protected function parseRevision( RevisionRecord $revision ): ContentThreadItemSet {
|
||||
$response = $this->requestRestbasePageHtml( $revision );
|
||||
|
||||
$doc = DOMUtils::parseHTML( $response['body'] );
|
||||
|
|
|
@ -5,6 +5,8 @@ namespace MediaWiki\Extension\DiscussionTools;
|
|||
use Html;
|
||||
use Language;
|
||||
use MediaWiki\Extension\DiscussionTools\Hooks\HookUtils;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentCommentItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentHeadingItem;
|
||||
use MediaWiki\MediaWikiServices;
|
||||
use MediaWiki\User\UserIdentity;
|
||||
use MWExceptionHandler;
|
||||
|
@ -81,9 +83,9 @@ class CommentFormatter {
|
|||
* Add a topic container around a heading element
|
||||
*
|
||||
* @param Element $headingElement Heading element
|
||||
* @param HeadingItem|null $headingItem Heading item
|
||||
* @param ContentHeadingItem|null $headingItem Heading item
|
||||
*/
|
||||
protected static function addTopicContainer( Element $headingElement, ?HeadingItem $headingItem = null ) {
|
||||
protected static function addTopicContainer( Element $headingElement, ?ContentHeadingItem $headingItem = null ) {
|
||||
$doc = $headingElement->ownerDocument;
|
||||
|
||||
DOMCompat::getClassList( $headingElement )->add( 'ext-discussiontools-init-section' );
|
||||
|
@ -182,7 +184,7 @@ class CommentFormatter {
|
|||
foreach ( array_reverse( $threadItems ) as $threadItem ) {
|
||||
// TODO: Consider not attaching JSON data to the DOM.
|
||||
// Create a dummy node to attach data to.
|
||||
if ( $threadItem instanceof HeadingItem && $threadItem->isPlaceholderHeading() ) {
|
||||
if ( $threadItem instanceof ContentHeadingItem && $threadItem->isPlaceholderHeading() ) {
|
||||
$node = $doc->createElement( 'span' );
|
||||
$container->insertBefore( $node, $container->firstChild );
|
||||
$threadItem->setRange( new ImmutableRange( $node, 0, $node, 0 ) );
|
||||
|
@ -218,7 +220,7 @@ class CommentFormatter {
|
|||
$itemData = $threadItem->jsonSerialize();
|
||||
$itemJSON = json_encode( $itemData );
|
||||
|
||||
if ( $threadItem instanceof HeadingItem ) {
|
||||
if ( $threadItem instanceof ContentHeadingItem ) {
|
||||
// <span class="mw-headline" …>, or <hN …> in Parsoid HTML
|
||||
$headline = $threadItem->getRange()->endContainer;
|
||||
Assert::precondition( $headline instanceof Element, 'HeadingItem refers to an element node' );
|
||||
|
@ -230,7 +232,7 @@ class CommentFormatter {
|
|||
static::addTopicContainer( $headingElement, $threadItem );
|
||||
}
|
||||
}
|
||||
} elseif ( $threadItem instanceof CommentItem ) {
|
||||
} elseif ( $threadItem instanceof ContentCommentItem ) {
|
||||
$replyLinkButtons = $doc->createElement( 'span' );
|
||||
$replyLinkButtons->setAttribute( 'class', 'ext-discussiontools-init-replylink-buttons' );
|
||||
|
||||
|
@ -465,10 +467,10 @@ class CommentFormatter {
|
|||
/**
|
||||
* Get JSON for a commentItem that can be inserted into a comment marker
|
||||
*
|
||||
* @param CommentItem $commentItem Comment item
|
||||
* @param ContentCommentItem $commentItem Comment item
|
||||
* @return string
|
||||
*/
|
||||
private static function getJsonForCommentMarker( CommentItem $commentItem ): string {
|
||||
private static function getJsonForCommentMarker( ContentCommentItem $commentItem ): string {
|
||||
$JSON = [
|
||||
'id' => $commentItem->getId(),
|
||||
'timestamp' => $commentItem->getTimestampString()
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace MediaWiki\Extension\DiscussionTools;
|
||||
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentCommentItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentThreadItem;
|
||||
use MediaWiki\MediaWikiServices;
|
||||
use MWException;
|
||||
use Wikimedia\Assert\Assert;
|
||||
|
@ -47,10 +49,10 @@ class CommentModifier {
|
|||
* Given a comment and a reply link, add the reply link to its document's DOM tree, at the end of
|
||||
* the comment.
|
||||
*
|
||||
* @param CommentItem $comment
|
||||
* @param ContentCommentItem $comment
|
||||
* @param Element $linkNode Reply link
|
||||
*/
|
||||
public static function addReplyLink( CommentItem $comment, Element $linkNode ): void {
|
||||
public static function addReplyLink( ContentCommentItem $comment, Element $linkNode ): void {
|
||||
$target = $comment->getRange()->endContainer;
|
||||
|
||||
// Insert the link before trailing whitespace.
|
||||
|
@ -77,13 +79,13 @@ class CommentModifier {
|
|||
* The DOM tree is suitably rearranged to ensure correct indentation level of the reply (wrapper
|
||||
* nodes are added, and other nodes may be moved around).
|
||||
*
|
||||
* @param ThreadItem $comment
|
||||
* @param ContentThreadItem $comment
|
||||
* @param string $replyIndentation Reply indentation syntax to use, one of:
|
||||
* - 'invisible' (use `<dl><dd>` tags to output `:` in wikitext)
|
||||
* - 'bullet' (use `<ul><li>` tags to output `*` in wikitext)
|
||||
* @return Element
|
||||
*/
|
||||
public static function addListItem( ThreadItem $comment, string $replyIndentation ): Element {
|
||||
public static function addListItem( ContentThreadItem $comment, string $replyIndentation ): Element {
|
||||
$listTypeMap = [
|
||||
'li' => 'ul',
|
||||
'dd' => 'dl'
|
||||
|
@ -466,10 +468,10 @@ class CommentModifier {
|
|||
/**
|
||||
* Add a reply to a specific comment
|
||||
*
|
||||
* @param ThreadItem $comment Comment being replied to
|
||||
* @param ContentThreadItem $comment Comment being replied to
|
||||
* @param DocumentFragment $container Container of comment DOM nodes
|
||||
*/
|
||||
public static function addReply( ThreadItem $comment, DocumentFragment $container ): void {
|
||||
public static function addReply( ContentThreadItem $comment, DocumentFragment $container ): void {
|
||||
$services = MediaWikiServices::getInstance();
|
||||
$dtConfig = $services->getConfigFactory()->makeConfig( 'discussiontools' );
|
||||
$replyIndentation = $dtConfig->get( 'DiscussionToolsReplyIndentation' );
|
||||
|
@ -573,11 +575,13 @@ class CommentModifier {
|
|||
/**
|
||||
* Add a reply in the DOM to a comment using wikitext.
|
||||
*
|
||||
* @param CommentItem $comment Comment being replied to
|
||||
* @param ContentCommentItem $comment Comment being replied to
|
||||
* @param string $wikitext
|
||||
* @param string|null $signature
|
||||
*/
|
||||
public static function addWikitextReply( CommentItem $comment, string $wikitext, string $signature = null ): void {
|
||||
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 ) {
|
||||
|
@ -589,11 +593,13 @@ class CommentModifier {
|
|||
/**
|
||||
* Add a reply in the DOM to a comment using HTML.
|
||||
*
|
||||
* @param CommentItem $comment Comment being replied to
|
||||
* @param ContentCommentItem $comment Comment being replied to
|
||||
* @param string $html
|
||||
* @param string|null $signature
|
||||
*/
|
||||
public static function addHtmlReply( CommentItem $comment, string $html, string $signature = null ): void {
|
||||
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 ) {
|
||||
|
|
|
@ -9,6 +9,11 @@ use DateTimeImmutable;
|
|||
use DateTimeZone;
|
||||
use Language;
|
||||
use MalformedTitleException;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\CommentItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentCommentItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentHeadingItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentThreadItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\ThreadItem;
|
||||
use MediaWiki\Languages\LanguageConverterFactory;
|
||||
use MWException;
|
||||
use TitleParser;
|
||||
|
@ -84,9 +89,9 @@ class CommentParser {
|
|||
*
|
||||
* @param Element $rootNode Root node of content to parse
|
||||
* @param TitleValue $title Title of the page being parsed
|
||||
* @return ThreadItemSet
|
||||
* @return ContentThreadItemSet
|
||||
*/
|
||||
public function parse( Element $rootNode, TitleValue $title ): ThreadItemSet {
|
||||
public function parse( Element $rootNode, TitleValue $title ): ContentThreadItemSet {
|
||||
$this->rootNode = $rootNode;
|
||||
$this->title = $title;
|
||||
|
||||
|
@ -784,8 +789,8 @@ class CommentParser {
|
|||
return $sigRange;
|
||||
}
|
||||
|
||||
private function buildThreadItems(): ThreadItemSet {
|
||||
$result = new ThreadItemSet();
|
||||
private function buildThreadItems(): ContentThreadItemSet {
|
||||
$result = new ContentThreadItemSet();
|
||||
|
||||
$timestampRegexps = $this->getLocalTimestampRegexps();
|
||||
$dfParsers = $this->getLocalTimestampParsers();
|
||||
|
@ -806,7 +811,7 @@ class CommentParser {
|
|||
$range = new ImmutableRange(
|
||||
$headingNode, $startOffset, $headingNode, $headingNode->childNodes->length
|
||||
);
|
||||
$curComment = new HeadingItem( $range, (int)( $match[ 1 ] ) );
|
||||
$curComment = new ContentHeadingItem( $range, (int)( $match[ 1 ] ) );
|
||||
$curComment->setRootNode( $this->rootNode );
|
||||
$result->addThreadItem( $curComment );
|
||||
$curCommentEnd = $node;
|
||||
|
@ -896,7 +901,7 @@ class CommentParser {
|
|||
$warnings[] = $dateWarning;
|
||||
}
|
||||
|
||||
$curComment = new CommentItem(
|
||||
$curComment = new ContentCommentItem(
|
||||
$level,
|
||||
$range,
|
||||
$sigRanges,
|
||||
|
@ -911,7 +916,7 @@ class CommentParser {
|
|||
// Add a fake placeholder heading if there are any comments in the 0th section
|
||||
// (before the first real heading)
|
||||
$range = new ImmutableRange( $this->rootNode, 0, $this->rootNode, 0 );
|
||||
$fakeHeading = new HeadingItem( $range, null );
|
||||
$fakeHeading = new ContentHeadingItem( $range, null );
|
||||
$fakeHeading->setRootNode( $this->rootNode );
|
||||
$result->addThreadItem( $fakeHeading );
|
||||
}
|
||||
|
@ -936,25 +941,25 @@ class CommentParser {
|
|||
/**
|
||||
* Given a thread item, return an identifier for it that is unique within the page.
|
||||
*
|
||||
* @param ThreadItem $threadItem
|
||||
* @param ThreadItemSet $previousItems
|
||||
* @param ContentThreadItem $threadItem
|
||||
* @param ContentThreadItemSet $previousItems
|
||||
* @return string
|
||||
*/
|
||||
private function computeId( ThreadItem $threadItem, ThreadItemSet $previousItems ): string {
|
||||
private function computeId( ContentThreadItem $threadItem, ContentThreadItemSet $previousItems ): string {
|
||||
// When changing the algorithm below, copy the old version into computeLegacyId()
|
||||
// for compatibility with cached data.
|
||||
|
||||
$id = null;
|
||||
|
||||
if ( $threadItem instanceof HeadingItem && $threadItem->isPlaceholderHeading() ) {
|
||||
if ( $threadItem instanceof ContentHeadingItem && $threadItem->isPlaceholderHeading() ) {
|
||||
// The range points to the root note, using it like below results in silly values
|
||||
$id = 'h-';
|
||||
} elseif ( $threadItem instanceof HeadingItem ) {
|
||||
} elseif ( $threadItem instanceof ContentHeadingItem ) {
|
||||
// <span class="mw-headline" …>, or <hN …> in Parsoid HTML
|
||||
$headline = $threadItem->getRange()->startContainer;
|
||||
Assert::precondition( $headline instanceof Element, 'HeadingItem refers to an element node' );
|
||||
$id = 'h-' . $this->truncateForId( $headline->getAttribute( 'id' ) ?? '' );
|
||||
} elseif ( $threadItem instanceof CommentItem ) {
|
||||
} elseif ( $threadItem instanceof ContentCommentItem ) {
|
||||
$id = 'c-' . $this->truncateForId( str_replace( ' ', '_', $threadItem->getAuthor() ) ) .
|
||||
'-' . $threadItem->getTimestampString();
|
||||
} else {
|
||||
|
@ -964,17 +969,17 @@ class CommentParser {
|
|||
// If there would be multiple comments with the same ID (i.e. the user left multiple comments
|
||||
// in one edit, or within a minute), add the parent ID to disambiguate them.
|
||||
$threadItemParent = $threadItem->getParent();
|
||||
if ( $threadItemParent instanceof HeadingItem && !$threadItemParent->isPlaceholderHeading() ) {
|
||||
if ( $threadItemParent instanceof ContentHeadingItem && !$threadItemParent->isPlaceholderHeading() ) {
|
||||
// <span class="mw-headline" …>, or <hN …> in Parsoid HTML
|
||||
$headline = $threadItemParent->getRange()->startContainer;
|
||||
Assert::precondition( $headline instanceof Element, 'HeadingItem refers to an element node' );
|
||||
$id .= '-' . $this->truncateForId( $headline->getAttribute( 'id' ) ?? '' );
|
||||
} elseif ( $threadItemParent instanceof CommentItem ) {
|
||||
} elseif ( $threadItemParent instanceof ContentCommentItem ) {
|
||||
$id .= '-' . $this->truncateForId( str_replace( ' ', '_', $threadItemParent->getAuthor() ) ) .
|
||||
'-' . $threadItemParent->getTimestampString();
|
||||
}
|
||||
|
||||
if ( $threadItem instanceof HeadingItem ) {
|
||||
if ( $threadItem instanceof ContentHeadingItem ) {
|
||||
// To avoid old threads re-appearing on popular pages when someone uses a vague title
|
||||
// (e.g. dozens of threads titled "question" on [[Wikipedia:Help desk]]: https://w.wiki/fbN),
|
||||
// include the oldest timestamp in the thread (i.e. date the thread was started) in the
|
||||
|
@ -1003,11 +1008,11 @@ class CommentParser {
|
|||
* Given a thread item, return an identifier for it like computeId(), generated according to an
|
||||
* older algorithm, so that we can still match IDs from cached data.
|
||||
*
|
||||
* @param ThreadItem $threadItem
|
||||
* @param ThreadItemSet $previousItems
|
||||
* @param ContentThreadItem $threadItem
|
||||
* @param ContentThreadItemSet $previousItems
|
||||
* @return string|null
|
||||
*/
|
||||
private function computeLegacyId( ThreadItem $threadItem, ThreadItemSet $previousItems ): ?string {
|
||||
private function computeLegacyId( ContentThreadItem $threadItem, ContentThreadItemSet $previousItems ): ?string {
|
||||
// When we change the algorithm in computeId(), the old version should be copied below
|
||||
// for compatibility with cached data.
|
||||
|
||||
|
@ -1021,16 +1026,16 @@ class CommentParser {
|
|||
*
|
||||
* Multiple comments on a page can have the same name; use ID to distinguish them.
|
||||
*
|
||||
* @param ThreadItem $threadItem
|
||||
* @param ContentThreadItem $threadItem
|
||||
* @return string
|
||||
*/
|
||||
private function computeName( ThreadItem $threadItem ): string {
|
||||
private function computeName( ContentThreadItem $threadItem ): string {
|
||||
$name = null;
|
||||
|
||||
if ( $threadItem instanceof HeadingItem ) {
|
||||
if ( $threadItem instanceof ContentHeadingItem ) {
|
||||
$name = 'h-';
|
||||
$mainComment = $this->getThreadStartComment( $threadItem );
|
||||
} elseif ( $threadItem instanceof CommentItem ) {
|
||||
} elseif ( $threadItem instanceof ContentCommentItem ) {
|
||||
$name = 'c-';
|
||||
$mainComment = $threadItem;
|
||||
} else {
|
||||
|
@ -1046,9 +1051,9 @@ class CommentParser {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param ThreadItemSet $result
|
||||
* @param ContentThreadItemSet $result
|
||||
*/
|
||||
private function buildThreads( ThreadItemSet $result ): void {
|
||||
private function buildThreads( ContentThreadItemSet $result ): void {
|
||||
$lastHeading = null;
|
||||
$replies = [];
|
||||
|
||||
|
@ -1062,7 +1067,7 @@ class CommentParser {
|
|||
}
|
||||
}
|
||||
|
||||
if ( $threadItem instanceof HeadingItem ) {
|
||||
if ( $threadItem instanceof ContentHeadingItem ) {
|
||||
// New root (thread)
|
||||
// Attach as a sub-thread to preceding higher-level heading.
|
||||
// Any replies will appear in the tree twice, under the main-thread and the sub-thread.
|
||||
|
@ -1094,9 +1099,9 @@ class CommentParser {
|
|||
* This has to be a separate pass because we don't have the list of replies before
|
||||
* this point.
|
||||
*
|
||||
* @param ThreadItemSet $result
|
||||
* @param ContentThreadItemSet $result
|
||||
*/
|
||||
private function computeIdsAndNames( ThreadItemSet $result ): void {
|
||||
private function computeIdsAndNames( ContentThreadItemSet $result ): void {
|
||||
foreach ( $result->getThreadItems() as $threadItem ) {
|
||||
$name = $this->computeName( $threadItem );
|
||||
$threadItem->setName( $name );
|
||||
|
@ -1116,14 +1121,14 @@ class CommentParser {
|
|||
*/
|
||||
private function getThreadStartComment( ThreadItem $threadItem ): ?CommentItem {
|
||||
$oldest = null;
|
||||
if ( $threadItem instanceof CommentItem ) {
|
||||
if ( $threadItem instanceof ContentCommentItem ) {
|
||||
$oldest = $threadItem;
|
||||
}
|
||||
// Check all replies. This can't just use the first comment because threads are often summarized
|
||||
// at the top when the discussion is closed.
|
||||
foreach ( $threadItem->getReplies() as $comment ) {
|
||||
// Don't include sub-threads to avoid changing the ID when threads are "merged".
|
||||
if ( $comment instanceof CommentItem ) {
|
||||
if ( $comment instanceof ContentCommentItem ) {
|
||||
$oldestInReplies = $this->getThreadStartComment( $comment );
|
||||
if ( !$oldest || $oldestInReplies->getTimestamp() < $oldest->getTimestamp() ) {
|
||||
$oldest = $oldestInReplies;
|
||||
|
|
|
@ -4,6 +4,8 @@ namespace MediaWiki\Extension\DiscussionTools;
|
|||
|
||||
use Config;
|
||||
use LogicException;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentCommentItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentThreadItem;
|
||||
use Wikimedia\Assert\Assert;
|
||||
use Wikimedia\Parsoid\DOM\Comment;
|
||||
use Wikimedia\Parsoid\DOM\Element;
|
||||
|
@ -389,13 +391,15 @@ class CommentUtils {
|
|||
/**
|
||||
* Get the nodes (if any) that contain the given thread item, and nothing else.
|
||||
*
|
||||
* @param ThreadItem $item
|
||||
* @param ContentThreadItem $item
|
||||
* @param ?Node $excludedAncestorNode Node that shouldn't be included in the result, even if it
|
||||
* contains the item and nothing else. This is intended to avoid traversing outside of a node
|
||||
* which is a container for all the thread items.
|
||||
* @return Node[]|null
|
||||
*/
|
||||
public static function getFullyCoveredSiblings( ThreadItem $item, ?Node $excludedAncestorNode = null ): ?array {
|
||||
public static function getFullyCoveredSiblings(
|
||||
ContentThreadItem $item, ?Node $excludedAncestorNode = null
|
||||
): ?array {
|
||||
$siblings = static::getCoveredSiblings( $item->getRange() );
|
||||
|
||||
$makeRange = static function ( $siblings ) {
|
||||
|
@ -710,13 +714,13 @@ class CommentUtils {
|
|||
* signature, or there's some text within the same paragraph that was detected as part of the same
|
||||
* comment).
|
||||
*
|
||||
* @param ThreadItemSet $itemSet
|
||||
* @param ContentThreadItemSet $itemSet
|
||||
* @param string $author
|
||||
* @param Element $rootNode
|
||||
* @return bool
|
||||
*/
|
||||
public static function isSingleCommentSignedBy(
|
||||
ThreadItemSet $itemSet,
|
||||
ContentThreadItemSet $itemSet,
|
||||
string $author,
|
||||
Element $rootNode
|
||||
): bool {
|
||||
|
@ -725,7 +729,7 @@ class CommentUtils {
|
|||
if ( $items ) {
|
||||
$lastItem = end( $items );
|
||||
// Check that we've detected a comment first, not just headings (T304377)
|
||||
if ( !( $lastItem instanceof CommentItem && $lastItem->getAuthor() === $author ) ) {
|
||||
if ( !( $lastItem instanceof ContentCommentItem && $lastItem->getAuthor() === $author ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
108
includes/ContentThreadItemSet.php
Normal file
108
includes/ContentThreadItemSet.php
Normal file
|
@ -0,0 +1,108 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Extension\DiscussionTools;
|
||||
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\CommentItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentCommentItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentHeadingItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentThreadItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\HeadingItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\ThreadItem;
|
||||
use Wikimedia\Assert\Assert;
|
||||
|
||||
/**
|
||||
* Groups thread items (headings and comments) generated by parsing a discussion page.
|
||||
*/
|
||||
class ContentThreadItemSet implements ThreadItemSet {
|
||||
|
||||
/** @var ContentThreadItem[] */
|
||||
private $threadItems = [];
|
||||
/** @var ContentCommentItem[] */
|
||||
private $commentItems = [];
|
||||
/** @var ContentThreadItem[][] */
|
||||
private $threadItemsByName = [];
|
||||
/** @var ContentThreadItem[] */
|
||||
private $threadItemsById = [];
|
||||
/** @var ContentHeadingItem[] */
|
||||
private $threads = [];
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @param ThreadItem $item
|
||||
*/
|
||||
public function addThreadItem( ThreadItem $item ) {
|
||||
Assert::precondition( $item instanceof ContentThreadItem, 'Must be ContentThreadItem' );
|
||||
|
||||
$this->threadItems[] = $item;
|
||||
if ( $item instanceof CommentItem ) {
|
||||
$this->commentItems[] = $item;
|
||||
}
|
||||
if ( $item instanceof HeadingItem ) {
|
||||
$this->threads[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function isEmpty(): bool {
|
||||
return !$this->threadItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @param ThreadItem $item
|
||||
*/
|
||||
public function updateIdAndNameMaps( ThreadItem $item ) {
|
||||
Assert::precondition( $item instanceof ContentThreadItem, 'Must be ContentThreadItem' );
|
||||
|
||||
$this->threadItemsByName[ $item->getName() ][] = $item;
|
||||
|
||||
$this->threadItemsById[ $item->getId() ] = $item;
|
||||
|
||||
$legacyId = $item->getLegacyId();
|
||||
if ( $legacyId ) {
|
||||
$this->threadItemsById[ $legacyId ] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return ContentThreadItem[] Thread items
|
||||
*/
|
||||
public function getThreadItems(): array {
|
||||
return $this->threadItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return ContentCommentItem[] Comment items
|
||||
*/
|
||||
public function getCommentItems(): array {
|
||||
return $this->commentItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return ContentThreadItem[] Thread items, empty array if not found
|
||||
*/
|
||||
public function findCommentsByName( string $name ): array {
|
||||
return $this->threadItemsByName[$name] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return ContentThreadItem|null Thread item, null if not found
|
||||
*/
|
||||
public function findCommentById( string $id ): ?ThreadItem {
|
||||
return $this->threadItemsById[$id] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return ContentHeadingItem[] Tree structure of comments, top-level items are the headings.
|
||||
*/
|
||||
public function getThreads(): array {
|
||||
return $this->threads;
|
||||
}
|
||||
}
|
103
includes/DatabaseThreadItemSet.php
Normal file
103
includes/DatabaseThreadItemSet.php
Normal file
|
@ -0,0 +1,103 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Extension\DiscussionTools;
|
||||
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\CommentItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\DatabaseCommentItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\DatabaseHeadingItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\DatabaseThreadItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\HeadingItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\ThreadItem;
|
||||
use Wikimedia\Assert\Assert;
|
||||
|
||||
/**
|
||||
* Groups thread items (headings and comments) generated from database.
|
||||
*/
|
||||
class DatabaseThreadItemSet implements ThreadItemSet {
|
||||
|
||||
/** @var DatabaseThreadItem[] */
|
||||
private $threadItems = [];
|
||||
/** @var DatabaseCommentItem[] */
|
||||
private $commentItems = [];
|
||||
/** @var DatabaseThreadItem[][] */
|
||||
private $threadItemsByName = [];
|
||||
/** @var DatabaseThreadItem[] */
|
||||
private $threadItemsById = [];
|
||||
/** @var DatabaseHeadingItem[] */
|
||||
private $threads = [];
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @param ThreadItem $item
|
||||
*/
|
||||
public function addThreadItem( ThreadItem $item ) {
|
||||
Assert::precondition( $item instanceof DatabaseThreadItem, 'Must be DatabaseThreadItem' );
|
||||
|
||||
$this->threadItems[] = $item;
|
||||
if ( $item instanceof CommentItem ) {
|
||||
$this->commentItems[] = $item;
|
||||
}
|
||||
if ( $item instanceof HeadingItem ) {
|
||||
$this->threads[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function isEmpty(): bool {
|
||||
return !$this->threadItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @param ThreadItem $item
|
||||
*/
|
||||
public function updateIdAndNameMaps( ThreadItem $item ) {
|
||||
Assert::precondition( $item instanceof DatabaseThreadItem, 'Must be DatabaseThreadItem' );
|
||||
|
||||
$this->threadItemsByName[ $item->getName() ][] = $item;
|
||||
|
||||
$this->threadItemsById[ $item->getId() ] = $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return DatabaseThreadItem[] Thread items
|
||||
*/
|
||||
public function getThreadItems(): array {
|
||||
return $this->threadItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return DatabaseCommentItem[] Comment items
|
||||
*/
|
||||
public function getCommentItems(): array {
|
||||
return $this->commentItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return DatabaseThreadItem[] Thread items, empty array if not found
|
||||
*/
|
||||
public function findCommentsByName( string $name ): array {
|
||||
return $this->threadItemsByName[$name] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return DatabaseThreadItem|null Thread item, null if not found
|
||||
*/
|
||||
public function findCommentById( string $id ): ?ThreadItem {
|
||||
return $this->threadItemsById[$id] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return DatabaseHeadingItem[] Tree structure of comments, top-level items are the headings.
|
||||
*/
|
||||
public function getThreads(): array {
|
||||
return $this->threads;
|
||||
}
|
||||
}
|
|
@ -18,13 +18,14 @@ use Error;
|
|||
use ExtensionRegistry;
|
||||
use IDBAccessObject;
|
||||
use Iterator;
|
||||
use MediaWiki\Extension\DiscussionTools\CommentItem;
|
||||
use MediaWiki\Extension\DiscussionTools\HeadingItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ContentThreadItemSet;
|
||||
use MediaWiki\Extension\DiscussionTools\Hooks\HookUtils;
|
||||
use MediaWiki\Extension\DiscussionTools\SubscriptionItem;
|
||||
use MediaWiki\Extension\DiscussionTools\SubscriptionStore;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItemSet;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\CommentItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentCommentItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentThreadItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\HeadingItem;
|
||||
use MediaWiki\Extension\EventLogging\EventLogging;
|
||||
use MediaWiki\MediaWikiServices;
|
||||
use MediaWiki\Page\PageIdentity;
|
||||
|
@ -41,9 +42,9 @@ use Wikimedia\Parsoid\Utils\DOMUtils;
|
|||
class EventDispatcher {
|
||||
/**
|
||||
* @param RevisionRecord $revRecord
|
||||
* @return ThreadItemSet
|
||||
* @return ContentThreadItemSet
|
||||
*/
|
||||
private static function getParsedRevision( RevisionRecord $revRecord ): ThreadItemSet {
|
||||
private static function getParsedRevision( RevisionRecord $revRecord ): ContentThreadItemSet {
|
||||
$services = MediaWikiServices::getInstance();
|
||||
|
||||
$pageRecord = $services->getPageStore()->getPageById( $revRecord->getPageId() ) ?:
|
||||
|
@ -121,8 +122,8 @@ class EventDispatcher {
|
|||
* For any other headings (including level 3+ before the first level 2 heading, level 1, and
|
||||
* section zero placeholder headings), ignore comments in those threads.
|
||||
*
|
||||
* @param ThreadItem[] $items
|
||||
* @return CommentItem[][][]
|
||||
* @param ContentThreadItem[] $items
|
||||
* @return ContentCommentItem[][][]
|
||||
*/
|
||||
private static function groupCommentsByThreadAndName( array $items ): array {
|
||||
$comments = [];
|
||||
|
@ -143,16 +144,16 @@ class EventDispatcher {
|
|||
* Helper for generateEventsForRevision(), separated out for easier testing.
|
||||
*
|
||||
* @param array &$events
|
||||
* @param ThreadItemSet $oldItemSet
|
||||
* @param ThreadItemSet $newItemSet
|
||||
* @param ContentThreadItemSet $oldItemSet
|
||||
* @param ContentThreadItemSet $newItemSet
|
||||
* @param RevisionRecord $newRevRecord
|
||||
* @param PageIdentity $title
|
||||
* @param UserIdentity $user
|
||||
*/
|
||||
protected static function generateEventsFromItemSets(
|
||||
array &$events,
|
||||
ThreadItemSet $oldItemSet,
|
||||
ThreadItemSet $newItemSet,
|
||||
ContentThreadItemSet $oldItemSet,
|
||||
ContentThreadItemSet $newItemSet,
|
||||
RevisionRecord $newRevRecord,
|
||||
PageIdentity $title,
|
||||
UserIdentity $user
|
||||
|
|
22
includes/ThreadItem/CommentItem.php
Normal file
22
includes/ThreadItem/CommentItem.php
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Extension\DiscussionTools\ThreadItem;
|
||||
|
||||
use DateTimeImmutable;
|
||||
|
||||
interface CommentItem extends ThreadItem {
|
||||
/**
|
||||
* @return string Comment author
|
||||
*/
|
||||
public function getAuthor(): string;
|
||||
|
||||
/**
|
||||
* @return DateTimeImmutable Comment timestamp
|
||||
*/
|
||||
public function getTimestamp(): DateTimeImmutable;
|
||||
|
||||
/**
|
||||
* @return string Comment timestamp in standard format
|
||||
*/
|
||||
public function getTimestampString(): string;
|
||||
}
|
94
includes/ThreadItem/CommentItemTrait.php
Normal file
94
includes/ThreadItem/CommentItemTrait.php
Normal file
|
@ -0,0 +1,94 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Extension\DiscussionTools\ThreadItem;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use MediaWiki\MediaWikiServices;
|
||||
use MWException;
|
||||
|
||||
trait CommentItemTrait {
|
||||
// phpcs:disable Squiz.WhiteSpace, MediaWiki.Commenting
|
||||
// Required ThreadItem methods (listed for Phan)
|
||||
abstract public function getParent(): ?ThreadItem;
|
||||
// Required CommentItem methods (listed for Phan)
|
||||
abstract public function getAuthor(): string;
|
||||
abstract public function getTimestamp(): DateTimeImmutable;
|
||||
// phpcs:enable
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @suppress PhanTraitParentReference
|
||||
*/
|
||||
public function jsonSerialize( bool $deep = false, ?callable $callback = null ): array {
|
||||
return array_merge( parent::jsonSerialize( $deep, $callback ), [
|
||||
'timestamp' => $this->getTimestampString(),
|
||||
'author' => $this->getAuthor(),
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array JSON-serializable array
|
||||
*/
|
||||
public function jsonSerializeForDiff(): array {
|
||||
$data = $this->jsonSerialize();
|
||||
|
||||
$heading = $this->getHeading();
|
||||
$data['headingId'] = $heading->getId();
|
||||
$subscribableHeading = $this->getSubscribableHeading();
|
||||
$data['subscribableHeadingId'] = $subscribableHeading ? $subscribableHeading->getId() : null;
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the comment timestamp in the format used in IDs and names.
|
||||
*
|
||||
* Depending on the date of the comment, this may use one of two formats:
|
||||
*
|
||||
* - For dates prior to 'DiscussionToolsTimestampFormatSwitchTime' (by default 2022-07-12):
|
||||
* Uses ISO 8601 date. Almost DateTimeInterface::RFC3339_EXTENDED, but ending with 'Z' instead
|
||||
* of '+00:00', like Date#toISOString in JavaScript.
|
||||
*
|
||||
* - For dates on or after 'DiscussionToolsTimestampFormatSwitchTime' (by default 2022-07-12):
|
||||
* Uses MediaWiki timestamp (TS_MW in MediaWiki PHP code).
|
||||
*
|
||||
* @return string Comment timestamp in standard format
|
||||
*/
|
||||
public function getTimestampString(): string {
|
||||
$dtConfig = MediaWikiServices::getInstance()->getConfigFactory()->makeConfig( 'discussiontools' );
|
||||
$switchTime = new DateTimeImmutable(
|
||||
$dtConfig->get( 'DiscussionToolsTimestampFormatSwitchTime' )
|
||||
);
|
||||
$timestamp = $this->getTimestamp();
|
||||
if ( $timestamp < $switchTime ) {
|
||||
return $timestamp->format( 'Y-m-d\TH:i:s.v\Z' );
|
||||
} else {
|
||||
return $timestamp->format( 'YmdHis' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ContentHeadingItem Closest ancestor which is a HeadingItem
|
||||
*/
|
||||
public function getHeading(): HeadingItem {
|
||||
$parent = $this;
|
||||
while ( $parent instanceof CommentItem ) {
|
||||
$parent = $parent->getParent();
|
||||
}
|
||||
if ( !( $parent instanceof HeadingItem ) ) {
|
||||
throw new MWException( 'heading parent not found' );
|
||||
}
|
||||
return $parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ContentHeadingItem|null Closest heading that can be used for topic subscriptions
|
||||
*/
|
||||
public function getSubscribableHeading(): ?HeadingItem {
|
||||
$heading = $this->getHeading();
|
||||
while ( $heading instanceof HeadingItem && !$heading->isSubscribable() ) {
|
||||
$heading = $heading->getParent();
|
||||
}
|
||||
return $heading instanceof HeadingItem ? $heading : null;
|
||||
}
|
||||
}
|
|
@ -1,10 +1,12 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Extension\DiscussionTools;
|
||||
namespace MediaWiki\Extension\DiscussionTools\ThreadItem;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use MediaWiki\Extension\DiscussionTools\CommentModifier;
|
||||
use MediaWiki\Extension\DiscussionTools\CommentUtils;
|
||||
use MediaWiki\Extension\DiscussionTools\ImmutableRange;
|
||||
use MediaWiki\MediaWikiServices;
|
||||
use MWException;
|
||||
use Sanitizer;
|
||||
use Title;
|
||||
use Wikimedia\Parsoid\DOM\DocumentFragment;
|
||||
|
@ -12,7 +14,12 @@ use Wikimedia\Parsoid\DOM\Text;
|
|||
use Wikimedia\Parsoid\Utils\DOMCompat;
|
||||
use Wikimedia\Parsoid\Utils\DOMUtils;
|
||||
|
||||
class CommentItem extends ThreadItem {
|
||||
class ContentCommentItem extends ContentThreadItem implements CommentItem {
|
||||
use CommentItemTrait {
|
||||
getHeading as protected traitGetHeading;
|
||||
getSubscribableHeading as protected traitGetSubscribableHeading;
|
||||
}
|
||||
|
||||
private $signatureRanges;
|
||||
private $timestamp;
|
||||
private $author;
|
||||
|
@ -37,30 +44,6 @@ class CommentItem extends ThreadItem {
|
|||
$this->author = $author;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function jsonSerialize( bool $deep = false, ?callable $callback = null ): array {
|
||||
return array_merge( parent::jsonSerialize( $deep, $callback ), [
|
||||
'timestamp' => $this->getTimestampString(),
|
||||
'author' => $this->author,
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array JSON-serializable array
|
||||
*/
|
||||
public function jsonSerializeForDiff(): array {
|
||||
$data = $this->jsonSerialize();
|
||||
|
||||
$heading = $this->getHeading();
|
||||
$data['headingId'] = $heading->getId();
|
||||
$subscribableHeading = $this->getSubscribableHeading();
|
||||
$data['subscribableHeadingId'] = $subscribableHeading ? $subscribableHeading->getId() : null;
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the HTML of this comment's body
|
||||
*
|
||||
|
@ -163,33 +146,6 @@ class CommentItem extends ThreadItem {
|
|||
return $this->timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the comment timestamp in the format used in IDs and names.
|
||||
*
|
||||
* Depending on the date of the comment, this may use one of two formats:
|
||||
*
|
||||
* - For dates prior to 'DiscussionToolsTimestampFormatSwitchTime' (by default 2022-07-12):
|
||||
* Uses ISO 8601 date. Almost DateTimeInterface::RFC3339_EXTENDED, but ending with 'Z' instead
|
||||
* of '+00:00', like Date#toISOString in JavaScript.
|
||||
*
|
||||
* - For dates on or after 'DiscussionToolsTimestampFormatSwitchTime' (by default 2022-07-12):
|
||||
* Uses MediaWiki timestamp (TS_MW in MediaWiki PHP code).
|
||||
*
|
||||
* @return string Comment timestamp in standard format
|
||||
*/
|
||||
public function getTimestampString(): string {
|
||||
$dtConfig = MediaWikiServices::getInstance()->getConfigFactory()->makeConfig( 'discussiontools' );
|
||||
$switchTime = new DateTimeImmutable(
|
||||
$dtConfig->get( 'DiscussionToolsTimestampFormatSwitchTime' )
|
||||
);
|
||||
$timestamp = $this->getTimestamp();
|
||||
if ( $timestamp < $switchTime ) {
|
||||
return $timestamp->format( 'Y-m-d\TH:i:s.v\Z' );
|
||||
} else {
|
||||
return $timestamp->format( 'YmdHis' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Comment author
|
||||
*/
|
||||
|
@ -198,28 +154,18 @@ class CommentItem extends ThreadItem {
|
|||
}
|
||||
|
||||
/**
|
||||
* @return HeadingItem Closest ancestor which is a HeadingItem
|
||||
* @inheritDoc CommentItemTrait::getHeading
|
||||
* @suppress PhanTypeMismatchReturnSuperType
|
||||
*/
|
||||
public function getHeading(): HeadingItem {
|
||||
$parent = $this;
|
||||
while ( $parent instanceof CommentItem ) {
|
||||
$parent = $parent->getParent();
|
||||
}
|
||||
if ( !( $parent instanceof HeadingItem ) ) {
|
||||
throw new MWException( 'heading parent not found' );
|
||||
}
|
||||
return $parent;
|
||||
public function getHeading(): ContentHeadingItem {
|
||||
return $this->traitGetHeading();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HeadingItem|null Closest heading that can be used for topic subscriptions
|
||||
* @inheritDoc CommentItemTrait::getSubscribableHeading
|
||||
*/
|
||||
public function getSubscribableHeading(): ?HeadingItem {
|
||||
$heading = $this->getHeading();
|
||||
while ( $heading instanceof HeadingItem && !$heading->isSubscribable() ) {
|
||||
$heading = $heading->getParent();
|
||||
}
|
||||
return $heading instanceof HeadingItem ? $heading : null;
|
||||
public function getSubscribableHeading(): ?ContentHeadingItem {
|
||||
return $this->traitGetSubscribableHeading();
|
||||
}
|
||||
|
||||
/**
|
|
@ -1,12 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Extension\DiscussionTools;
|
||||
namespace MediaWiki\Extension\DiscussionTools\ThreadItem;
|
||||
|
||||
use MediaWiki\Extension\DiscussionTools\ImmutableRange;
|
||||
use Wikimedia\Assert\Assert;
|
||||
use Wikimedia\Parsoid\DOM\Element;
|
||||
|
||||
class HeadingItem extends ThreadItem {
|
||||
private $placeholderHeading = false;
|
||||
class ContentHeadingItem extends ContentThreadItem implements HeadingItem {
|
||||
use HeadingItemTrait;
|
||||
|
||||
/** @var bool */
|
||||
private $placeholderHeading;
|
||||
/** @var int */
|
||||
private $headingLevel;
|
||||
|
||||
// Placeholder headings must have a level higher than real headings (1-6)
|
||||
|
@ -24,18 +29,6 @@ class HeadingItem extends ThreadItem {
|
|||
$this->headingLevel = $this->placeholderHeading ? static::PLACEHOLDER_HEADING_LEVEL : $headingLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function jsonSerialize( bool $deep = false, ?callable $callback = null ): array {
|
||||
return array_merge( parent::jsonSerialize( $deep, $callback ), [
|
||||
'headingLevel' => $this->headingLevel === static::PLACEHOLDER_HEADING_LEVEL ? null : $this->headingLevel,
|
||||
// Used for topic subscriptions. Not added to CommentItem's yet as there is
|
||||
// no use case for it.
|
||||
'name' => $this->name,
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a title based on the hash ID, such that it can be linked to
|
||||
*
|
||||
|
@ -88,24 +81,6 @@ class HeadingItem extends ThreadItem {
|
|||
$this->placeholderHeading = $placeholderHeading;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether this heading can be used for topic subscriptions.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isSubscribable(): bool {
|
||||
return (
|
||||
// Placeholder headings have nothing to attach the button to.
|
||||
!$this->isPlaceholderHeading() &&
|
||||
// We only allow subscribing to level 2 headings, because the user interface for sub-headings
|
||||
// would be difficult to present.
|
||||
$this->getHeadingLevel() === 2 &&
|
||||
// Check if the name corresponds to a section that contain no comments (only sub-sections).
|
||||
// They can't be distinguished from each other, so disallow subscribing.
|
||||
$this->getName() !== 'h-'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
|
@ -1,9 +1,12 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Extension\DiscussionTools;
|
||||
namespace MediaWiki\Extension\DiscussionTools\ThreadItem;
|
||||
|
||||
use JsonSerializable;
|
||||
use LogicException;
|
||||
use MediaWiki\Extension\DiscussionTools\CommentModifier;
|
||||
use MediaWiki\Extension\DiscussionTools\CommentUtils;
|
||||
use MediaWiki\Extension\DiscussionTools\ImmutableRange;
|
||||
use Sanitizer;
|
||||
use Title;
|
||||
use Wikimedia\Assert\Assert;
|
||||
|
@ -13,7 +16,9 @@ use Wikimedia\Parsoid\Utils\DOMUtils;
|
|||
/**
|
||||
* A thread item, either a heading or a comment
|
||||
*/
|
||||
abstract class ThreadItem implements JsonSerializable {
|
||||
abstract class ContentThreadItem implements JsonSerializable, ThreadItem {
|
||||
use ThreadItemTrait;
|
||||
|
||||
protected $type;
|
||||
protected $range;
|
||||
protected $rootNode;
|
||||
|
@ -40,31 +45,6 @@ abstract class ThreadItem implements JsonSerializable {
|
|||
$this->range = $range;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $deep Whether to include full serialized comments in the replies key
|
||||
* @param callable|null $callback Function to call on the returned serialized array, which
|
||||
* will be passed into the serialized replies as well if $deep is used
|
||||
* @return array JSON-serializable array
|
||||
*/
|
||||
public function jsonSerialize( bool $deep = false, ?callable $callback = null ): array {
|
||||
// The output of this method can end up in the HTTP cache (Varnish). Avoid changing it;
|
||||
// and when doing so, ensure that frontend code can handle both the old and new outputs.
|
||||
// See ThreadItem.static.newFromJSON in JS.
|
||||
|
||||
$array = [
|
||||
'type' => $this->type,
|
||||
'level' => $this->level,
|
||||
'id' => $this->id,
|
||||
'replies' => array_map( static function ( ThreadItem $comment ) use ( $deep, $callback ) {
|
||||
return $deep ? $comment->jsonSerialize( $deep, $callback ) : $comment->getId();
|
||||
}, $this->replies )
|
||||
];
|
||||
if ( $callback ) {
|
||||
$callback( $array, $this );
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get summary metadata for a thread.
|
||||
*
|
||||
|
@ -135,7 +115,7 @@ abstract class ThreadItem implements JsonSerializable {
|
|||
/**
|
||||
* Get the list of thread items in the comment tree below this thread item.
|
||||
*
|
||||
* @return ThreadItem[] Thread items
|
||||
* @return ContentThreadItem[] Thread items
|
||||
*/
|
||||
public function getThreadItemsBelow(): array {
|
||||
$threadItems = [];
|
||||
|
@ -370,7 +350,7 @@ abstract class ThreadItem implements JsonSerializable {
|
|||
}
|
||||
|
||||
/**
|
||||
* @return ThreadItem|null Parent thread item
|
||||
* @return ContentThreadItem|null Parent thread item
|
||||
*/
|
||||
public function getParent(): ?ThreadItem {
|
||||
return $this->parent;
|
||||
|
@ -412,7 +392,7 @@ abstract class ThreadItem implements JsonSerializable {
|
|||
}
|
||||
|
||||
/**
|
||||
* @return ThreadItem[] Replies to this thread item
|
||||
* @return ContentThreadItem[] Replies to this thread item
|
||||
*/
|
||||
public function getReplies(): array {
|
||||
return $this->replies;
|
||||
|
@ -433,9 +413,9 @@ abstract class ThreadItem implements JsonSerializable {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param ThreadItem $parent
|
||||
* @param ContentThreadItem $parent
|
||||
*/
|
||||
public function setParent( ThreadItem $parent ): void {
|
||||
public function setParent( ContentThreadItem $parent ): void {
|
||||
$this->parent = $parent;
|
||||
}
|
||||
|
||||
|
@ -489,9 +469,9 @@ abstract class ThreadItem implements JsonSerializable {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param ThreadItem $reply Reply comment
|
||||
* @param ContentThreadItem $reply Reply comment
|
||||
*/
|
||||
public function addReply( ThreadItem $reply ): void {
|
||||
public function addReply( ContentThreadItem $reply ): void {
|
||||
$this->replies[] = $reply;
|
||||
}
|
||||
}
|
64
includes/ThreadItem/DatabaseCommentItem.php
Normal file
64
includes/ThreadItem/DatabaseCommentItem.php
Normal file
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Extension\DiscussionTools\ThreadItem;
|
||||
|
||||
use DateTimeImmutable;
|
||||
|
||||
class DatabaseCommentItem extends DatabaseThreadItem implements CommentItem {
|
||||
use CommentItemTrait {
|
||||
getHeading as protected traitGetHeading;
|
||||
getSubscribableHeading as protected traitGetSubscribableHeading;
|
||||
}
|
||||
|
||||
/** @var string */
|
||||
private $timestamp;
|
||||
/** @var string */
|
||||
private $author;
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param string $id
|
||||
* @param DatabaseThreadItem|null $parent
|
||||
* @param bool|string $transcludedFrom
|
||||
* @param int $level
|
||||
* @param string $timestamp
|
||||
* @param string $author
|
||||
*/
|
||||
public function __construct(
|
||||
string $name, string $id, ?DatabaseThreadItem $parent, $transcludedFrom, int $level,
|
||||
string $timestamp, string $author
|
||||
) {
|
||||
parent::__construct( 'comment', $name, $id, $parent, $transcludedFrom, $level );
|
||||
$this->timestamp = $timestamp;
|
||||
$this->author = $author;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getAuthor(): string {
|
||||
return $this->author;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getTimestamp(): DateTimeImmutable {
|
||||
return new DateTimeImmutable( $this->timestamp );
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc CommentItemTrait::getHeading
|
||||
* @suppress PhanTypeMismatchReturnSuperType
|
||||
*/
|
||||
public function getHeading(): DatabaseHeadingItem {
|
||||
return $this->traitGetHeading();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc CommentItemTrait::getSubscribableHeading
|
||||
*/
|
||||
public function getSubscribableHeading(): ?DatabaseHeadingItem {
|
||||
return $this->traitGetSubscribableHeading();
|
||||
}
|
||||
}
|
46
includes/ThreadItem/DatabaseHeadingItem.php
Normal file
46
includes/ThreadItem/DatabaseHeadingItem.php
Normal file
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Extension\DiscussionTools\ThreadItem;
|
||||
|
||||
class DatabaseHeadingItem extends DatabaseThreadItem implements HeadingItem {
|
||||
use HeadingItemTrait;
|
||||
|
||||
/** @var bool */
|
||||
private $placeholderHeading;
|
||||
/** @var int */
|
||||
private $headingLevel;
|
||||
|
||||
// Placeholder headings must have a level higher than real headings (1-6)
|
||||
private const PLACEHOLDER_HEADING_LEVEL = 99;
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param string $id
|
||||
* @param DatabaseThreadItem|null $parent
|
||||
* @param bool|string $transcludedFrom
|
||||
* @param int $level
|
||||
* @param ?int $headingLevel Heading level (1-6). Use null for a placeholder heading.
|
||||
*/
|
||||
public function __construct(
|
||||
string $name, string $id, ?DatabaseThreadItem $parent, $transcludedFrom, int $level,
|
||||
?int $headingLevel
|
||||
) {
|
||||
parent::__construct( 'heading', $name, $id, $parent, $transcludedFrom, $level );
|
||||
$this->placeholderHeading = $headingLevel === null;
|
||||
$this->headingLevel = $this->placeholderHeading ? static::PLACEHOLDER_HEADING_LEVEL : $headingLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getHeadingLevel(): int {
|
||||
return $this->headingLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function isPlaceholderHeading(): bool {
|
||||
return $this->placeholderHeading;
|
||||
}
|
||||
}
|
101
includes/ThreadItem/DatabaseThreadItem.php
Normal file
101
includes/ThreadItem/DatabaseThreadItem.php
Normal file
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Extension\DiscussionTools\ThreadItem;
|
||||
|
||||
use JsonSerializable;
|
||||
|
||||
class DatabaseThreadItem implements JsonSerializable, ThreadItem {
|
||||
use ThreadItemTrait;
|
||||
|
||||
/** @var string */
|
||||
private $type;
|
||||
/** @var string */
|
||||
private $name;
|
||||
/** @var string */
|
||||
private $id;
|
||||
/** @var DatabaseThreadItem|null */
|
||||
private $parent;
|
||||
/** @var DatabaseThreadItem[] */
|
||||
private $replies = [];
|
||||
/** @var string|bool */
|
||||
private $transcludedFrom;
|
||||
/** @var int */
|
||||
private $level;
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @param string $name
|
||||
* @param string $id
|
||||
* @param DatabaseThreadItem|null $parent
|
||||
* @param bool|string $transcludedFrom
|
||||
* @param int $level
|
||||
*/
|
||||
public function __construct(
|
||||
string $type, string $name, string $id, ?DatabaseThreadItem $parent, $transcludedFrom, int $level
|
||||
) {
|
||||
$this->name = $name;
|
||||
$this->id = $id;
|
||||
$this->type = $type;
|
||||
$this->parent = $parent;
|
||||
$this->transcludedFrom = $transcludedFrom;
|
||||
$this->level = $level;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getName(): string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DatabaseThreadItem $reply Reply comment
|
||||
*/
|
||||
public function addReply( DatabaseThreadItem $reply ): void {
|
||||
$this->replies[] = $reply;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getId(): string {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getType(): string {
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return DatabaseThreadItem|null
|
||||
*/
|
||||
public function getParent(): ?ThreadItem {
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return DatabaseThreadItem[]
|
||||
*/
|
||||
public function getReplies(): array {
|
||||
return $this->replies;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getTranscludedFrom() {
|
||||
return $this->transcludedFrom;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getLevel(): int {
|
||||
return $this->level;
|
||||
}
|
||||
}
|
15
includes/ThreadItem/HeadingItem.php
Normal file
15
includes/ThreadItem/HeadingItem.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Extension\DiscussionTools\ThreadItem;
|
||||
|
||||
interface HeadingItem extends ThreadItem {
|
||||
/**
|
||||
* @return int Heading level (1-6)
|
||||
*/
|
||||
public function getHeadingLevel(): int;
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isPlaceholderHeading(): bool;
|
||||
}
|
44
includes/ThreadItem/HeadingItemTrait.php
Normal file
44
includes/ThreadItem/HeadingItemTrait.php
Normal file
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Extension\DiscussionTools\ThreadItem;
|
||||
|
||||
trait HeadingItemTrait {
|
||||
// phpcs:disable Squiz.WhiteSpace, MediaWiki.Commenting
|
||||
// Required ThreadItem methods (listed for Phan)
|
||||
abstract public function getName(): string;
|
||||
// Required HeadingItem methods (listed for Phan)
|
||||
abstract public function getHeadingLevel(): int;
|
||||
abstract public function isPlaceholderHeading(): bool;
|
||||
// phpcs:enable
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @suppress PhanTraitParentReference
|
||||
*/
|
||||
public function jsonSerialize( bool $deep = false, ?callable $callback = null ): array {
|
||||
return array_merge( parent::jsonSerialize( $deep, $callback ), [
|
||||
'headingLevel' => $this->isPlaceholderHeading() ? null : $this->getHeadingLevel(),
|
||||
// Used for topic subscriptions. Not added to CommentItem's yet as there is
|
||||
// no use case for it.
|
||||
'name' => $this->getName(),
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether this heading can be used for topic subscriptions.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isSubscribable(): bool {
|
||||
return (
|
||||
// Placeholder headings have nothing to attach the button to.
|
||||
!$this->isPlaceholderHeading() &&
|
||||
// We only allow subscribing to level 2 headings, because the user interface for sub-headings
|
||||
// would be difficult to present.
|
||||
$this->getHeadingLevel() === 2 &&
|
||||
// Check if the name corresponds to a section that contain no comments (only sub-sections).
|
||||
// They can't be distinguished from each other, so disallow subscribing.
|
||||
$this->getName() !== 'h-'
|
||||
);
|
||||
}
|
||||
}
|
53
includes/ThreadItem/ThreadItem.php
Normal file
53
includes/ThreadItem/ThreadItem.php
Normal file
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Extension\DiscussionTools\ThreadItem;
|
||||
|
||||
/**
|
||||
* A thread item, either a heading or a comment
|
||||
*/
|
||||
interface ThreadItem {
|
||||
/**
|
||||
* @return string Thread ID
|
||||
*/
|
||||
public function getId(): string;
|
||||
|
||||
/**
|
||||
* @return string Thread item name
|
||||
*/
|
||||
public function getName(): string;
|
||||
|
||||
/**
|
||||
* @return string Thread item type
|
||||
*/
|
||||
public function getType(): string;
|
||||
|
||||
/**
|
||||
* @return ThreadItem|null Parent thread item
|
||||
*/
|
||||
public function getParent(): ?ThreadItem;
|
||||
|
||||
/**
|
||||
* @return ThreadItem[] Replies to this thread item
|
||||
*/
|
||||
public function getReplies(): array;
|
||||
|
||||
/**
|
||||
* @return string|bool `false` if this item is not transcluded. A string if it's transcluded
|
||||
* from a single page (the page title, in text form with spaces). `true` if it's transcluded, but
|
||||
* we can't determine the source.
|
||||
*/
|
||||
public function getTranscludedFrom();
|
||||
|
||||
/**
|
||||
* @return int Indentation level
|
||||
*/
|
||||
public function getLevel(): int;
|
||||
|
||||
/**
|
||||
* @param bool $deep Whether to include full serialized comments in the replies key
|
||||
* @param callable|null $callback Function to call on the returned serialized array, which
|
||||
* will be passed into the serialized replies as well if $deep is used
|
||||
* @return array JSON-serializable array
|
||||
*/
|
||||
public function jsonSerialize( bool $deep = false, ?callable $callback = null ): array;
|
||||
}
|
38
includes/ThreadItem/ThreadItemTrait.php
Normal file
38
includes/ThreadItem/ThreadItemTrait.php
Normal file
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Extension\DiscussionTools\ThreadItem;
|
||||
|
||||
trait ThreadItemTrait {
|
||||
// phpcs:disable Squiz.WhiteSpace, MediaWiki.Commenting
|
||||
// Required ThreadItem methods (listed for Phan)
|
||||
abstract public function getId(): string;
|
||||
abstract public function getType(): string;
|
||||
abstract public function getReplies(): array;
|
||||
abstract public function getLevel(): int;
|
||||
// phpcs:enable
|
||||
|
||||
/**
|
||||
* @param bool $deep Whether to include full serialized comments in the replies key
|
||||
* @param callable|null $callback Function to call on the returned serialized array, which
|
||||
* will be passed into the serialized replies as well if $deep is used
|
||||
* @return array JSON-serializable array
|
||||
*/
|
||||
public function jsonSerialize( bool $deep = false, ?callable $callback = null ): array {
|
||||
// The output of this method can end up in the HTTP cache (Varnish). Avoid changing it;
|
||||
// and when doing so, ensure that frontend code can handle both the old and new outputs.
|
||||
// See ThreadItem.static.newFromJSON in JS.
|
||||
|
||||
$array = [
|
||||
'type' => $this->getType(),
|
||||
'level' => $this->getLevel(),
|
||||
'id' => $this->getId(),
|
||||
'replies' => array_map( static function ( ThreadItem $comment ) use ( $deep, $callback ) {
|
||||
return $deep ? $comment->jsonSerialize( $deep, $callback ) : $comment->getId();
|
||||
}, $this->getReplies() )
|
||||
];
|
||||
if ( $callback ) {
|
||||
$callback( $array, $this );
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
}
|
|
@ -2,58 +2,31 @@
|
|||
|
||||
namespace MediaWiki\Extension\DiscussionTools;
|
||||
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\CommentItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\HeadingItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\ThreadItem;
|
||||
|
||||
/**
|
||||
* Groups thread items (headings and comments) generated by parsing a discussion page.
|
||||
*/
|
||||
class ThreadItemSet {
|
||||
|
||||
/** @var ThreadItem[] */
|
||||
private $threadItems = [];
|
||||
/** @var CommentItem[] */
|
||||
private $commentItems = [];
|
||||
/** @var ThreadItem[][] */
|
||||
private $threadItemsByName = [];
|
||||
/** @var ThreadItem[] */
|
||||
private $threadItemsById = [];
|
||||
/** @var HeadingItem[] */
|
||||
private $threads = [];
|
||||
|
||||
interface ThreadItemSet {
|
||||
/**
|
||||
* @internal Only used by CommentParser
|
||||
* @param ThreadItem $item
|
||||
* @internal Only used by CommentParser
|
||||
*/
|
||||
public function addThreadItem( ThreadItem $item ) {
|
||||
$this->threadItems[] = $item;
|
||||
if ( $item instanceof CommentItem ) {
|
||||
$this->commentItems[] = $item;
|
||||
}
|
||||
if ( $item instanceof HeadingItem ) {
|
||||
$this->threads[] = $item;
|
||||
}
|
||||
}
|
||||
public function addThreadItem( ThreadItem $item );
|
||||
|
||||
/**
|
||||
* @internal Only used by CommentParser
|
||||
* @return bool
|
||||
* @internal Only used by CommentParser
|
||||
*/
|
||||
public function isEmpty(): bool {
|
||||
return !$this->threadItems;
|
||||
}
|
||||
public function isEmpty(): bool;
|
||||
|
||||
/**
|
||||
* @internal Only used by CommentParser
|
||||
* @param ThreadItem $item
|
||||
* @internal Only used by CommentParser
|
||||
*/
|
||||
public function updateIdAndNameMaps( ThreadItem $item ) {
|
||||
$this->threadItemsByName[ $item->getName() ][] = $item;
|
||||
|
||||
$this->threadItemsById[ $item->getId() ] = $item;
|
||||
|
||||
$legacyId = $item->getLegacyId();
|
||||
if ( $legacyId ) {
|
||||
$this->threadItemsById[ $legacyId ] = $item;
|
||||
}
|
||||
}
|
||||
public function updateIdAndNameMaps( ThreadItem $item );
|
||||
|
||||
/**
|
||||
* Get all discussion comments (and headings) within a DOM subtree.
|
||||
|
@ -90,18 +63,14 @@ class ThreadItemSet {
|
|||
*
|
||||
* @return ThreadItem[] Thread items
|
||||
*/
|
||||
public function getThreadItems(): array {
|
||||
return $this->threadItems;
|
||||
}
|
||||
public function getThreadItems(): array;
|
||||
|
||||
/**
|
||||
* Same as getFlatThreadItems, but only returns the CommentItems
|
||||
*
|
||||
* @return CommentItem[] Comment items
|
||||
*/
|
||||
public function getCommentItems(): array {
|
||||
return $this->commentItems;
|
||||
}
|
||||
public function getCommentItems(): array;
|
||||
|
||||
/**
|
||||
* Find ThreadItems by their name
|
||||
|
@ -112,9 +81,7 @@ class ThreadItemSet {
|
|||
* @param string $name Name
|
||||
* @return ThreadItem[] Thread items, empty array if not found
|
||||
*/
|
||||
public function findCommentsByName( string $name ): array {
|
||||
return $this->threadItemsByName[$name] ?? [];
|
||||
}
|
||||
public function findCommentsByName( string $name ): array;
|
||||
|
||||
/**
|
||||
* Find a ThreadItem by its ID
|
||||
|
@ -122,9 +89,7 @@ class ThreadItemSet {
|
|||
* @param string $id ID
|
||||
* @return ThreadItem|null Thread item, null if not found
|
||||
*/
|
||||
public function findCommentById( string $id ): ?ThreadItem {
|
||||
return $this->threadItemsById[$id] ?? null;
|
||||
}
|
||||
public function findCommentById( string $id ): ?ThreadItem;
|
||||
|
||||
/**
|
||||
* Group discussion comments into threads and associate replies to original messages.
|
||||
|
@ -169,7 +134,5 @@ class ThreadItemSet {
|
|||
*
|
||||
* @return HeadingItem[] Tree structure of comments, top-level items are the headings.
|
||||
*/
|
||||
public function getThreads(): array {
|
||||
return $this->threads;
|
||||
}
|
||||
public function getThreads(): array;
|
||||
}
|
||||
|
|
|
@ -4,11 +4,11 @@ namespace MediaWiki\Extension\DiscussionTools\Tests;
|
|||
|
||||
use DateTimeImmutable;
|
||||
use Error;
|
||||
use MediaWiki\Extension\DiscussionTools\CommentItem;
|
||||
use MediaWiki\Extension\DiscussionTools\CommentUtils;
|
||||
use MediaWiki\Extension\DiscussionTools\HeadingItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ImmutableRange;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentCommentItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentHeadingItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentThreadItem;
|
||||
use MediaWiki\MediaWikiServices;
|
||||
use stdClass;
|
||||
use Wikimedia\Parsoid\DOM\Element;
|
||||
|
@ -54,16 +54,16 @@ class CommentParserTest extends IntegrationTestCase {
|
|||
return implode( '/', $path );
|
||||
}
|
||||
|
||||
private static function serializeComments( ThreadItem $threadItem, Element $root ): stdClass {
|
||||
private static function serializeComments( ContentThreadItem $threadItem, Element $root ): stdClass {
|
||||
$serialized = new stdClass();
|
||||
|
||||
if ( $threadItem instanceof HeadingItem ) {
|
||||
if ( $threadItem instanceof ContentHeadingItem ) {
|
||||
$serialized->placeholderHeading = $threadItem->isPlaceholderHeading();
|
||||
}
|
||||
|
||||
$serialized->type = $threadItem->getType();
|
||||
|
||||
if ( $threadItem instanceof CommentItem ) {
|
||||
if ( $threadItem instanceof ContentCommentItem ) {
|
||||
$serialized->timestamp = $threadItem->getTimestampString();
|
||||
$serialized->author = $threadItem->getAuthor();
|
||||
}
|
||||
|
@ -76,7 +76,7 @@ class CommentParserTest extends IntegrationTestCase {
|
|||
static::getOffsetPath( $root, $range->endContainer, $range->endOffset )
|
||||
];
|
||||
|
||||
if ( $threadItem instanceof CommentItem ) {
|
||||
if ( $threadItem instanceof ContentCommentItem ) {
|
||||
$serialized->signatureRanges = array_map( function ( ImmutableRange $range ) use ( $root ) {
|
||||
return [
|
||||
static::getOffsetPath( $root, $range->startContainer, $range->startOffset ),
|
||||
|
@ -85,7 +85,7 @@ class CommentParserTest extends IntegrationTestCase {
|
|||
}, $threadItem->getSignatureRanges() );
|
||||
}
|
||||
|
||||
if ( $threadItem instanceof HeadingItem ) {
|
||||
if ( $threadItem instanceof ContentHeadingItem ) {
|
||||
$serialized->headingLevel = $threadItem->getHeadingLevel();
|
||||
}
|
||||
$serialized->level = $threadItem->getLevel();
|
||||
|
|
|
@ -3,19 +3,20 @@
|
|||
namespace MediaWiki\Extension\DiscussionTools\Tests;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use MediaWiki\Extension\DiscussionTools\CommentItem;
|
||||
use MediaWiki\Extension\DiscussionTools\CommentUtils;
|
||||
use MediaWiki\Extension\DiscussionTools\HeadingItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ImmutableRange;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentCommentItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentHeadingItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentThreadItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\ThreadItem;
|
||||
use MediaWiki\MediaWikiServices;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \MediaWiki\Extension\DiscussionTools\ThreadItem
|
||||
* @coversDefaultClass \MediaWiki\Extension\DiscussionTools\ThreadItem\ContentThreadItem
|
||||
*
|
||||
* @group DiscussionTools
|
||||
*/
|
||||
class ThreadItemTest extends IntegrationTestCase {
|
||||
class ContentThreadItemTest extends IntegrationTestCase {
|
||||
/**
|
||||
* @dataProvider provideAuthors
|
||||
* @covers ::getAuthorsBelow
|
||||
|
@ -28,11 +29,11 @@ class ThreadItemTest extends IntegrationTestCase {
|
|||
$node = $doc->createElement( 'div' );
|
||||
$range = new ImmutableRange( $node, 0, $node, 0 );
|
||||
|
||||
$makeThreadItem = static function ( array $arr ) use ( &$makeThreadItem, $range ): ThreadItem {
|
||||
$makeThreadItem = static function ( array $arr ) use ( &$makeThreadItem, $range ): ContentThreadItem {
|
||||
if ( $arr['type'] === 'comment' ) {
|
||||
$item = new CommentItem( 1, $range, [], new DateTimeImmutable(), $arr['author'] );
|
||||
$item = new ContentCommentItem( 1, $range, [], new DateTimeImmutable(), $arr['author'] );
|
||||
} else {
|
||||
$item = new HeadingItem( $range, 2 );
|
||||
$item = new ContentHeadingItem( $range, 2 );
|
||||
}
|
||||
$item->setId( $arr['id'] );
|
||||
foreach ( $arr['replies'] as $reply ) {
|
||||
|
@ -102,7 +103,7 @@ class ThreadItemTest extends IntegrationTestCase {
|
|||
/**
|
||||
* @dataProvider provideGetText
|
||||
* @covers ::getText
|
||||
* @covers \MediaWiki\Extension\DiscussionTools\CommentItem::getBodyText
|
||||
* @covers \MediaWiki\Extension\DiscussionTools\ThreadItem\ContentCommentItem::getBodyText
|
||||
* @covers \MediaWiki\Extension\DiscussionTools\ImmutableRange::cloneContents
|
||||
*/
|
||||
public function testGetText(
|
||||
|
@ -125,7 +126,7 @@ class ThreadItemTest extends IntegrationTestCase {
|
|||
$output = [];
|
||||
foreach ( $items as $item ) {
|
||||
$output[ $item->getId() ] = CommentUtils::htmlTrim(
|
||||
$item instanceof CommentItem ? $item->getBodyText( true ) : $item->getText()
|
||||
$item instanceof ContentCommentItem ? $item->getBodyText( true ) : $item->getText()
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -148,7 +149,7 @@ class ThreadItemTest extends IntegrationTestCase {
|
|||
/**
|
||||
* @dataProvider provideGetHTML
|
||||
* @covers ::getHTML
|
||||
* @covers \MediaWiki\Extension\DiscussionTools\CommentItem::getBodyHTML
|
||||
* @covers \MediaWiki\Extension\DiscussionTools\ThreadItem\ContentCommentItem::getBodyHTML
|
||||
* @covers \MediaWiki\Extension\DiscussionTools\ImmutableRange::cloneContents
|
||||
*/
|
||||
public function testGetHTML(
|
||||
|
@ -171,7 +172,7 @@ class ThreadItemTest extends IntegrationTestCase {
|
|||
$output = [];
|
||||
foreach ( $items as $item ) {
|
||||
$output[ $item->getId() ] = CommentUtils::htmlTrim(
|
||||
$item instanceof CommentItem ? $item->getBodyHTML( true ) : $item->getHTML()
|
||||
$item instanceof ContentCommentItem ? $item->getBodyHTML( true ) : $item->getHTML()
|
||||
);
|
||||
}
|
||||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
namespace MediaWiki\Extension\DiscussionTools\Tests;
|
||||
|
||||
use MediaWiki\Extension\DiscussionTools\ContentThreadItemSet;
|
||||
use MediaWiki\Extension\DiscussionTools\Notifications\EventDispatcher;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItemSet;
|
||||
use MediaWiki\Page\PageIdentity;
|
||||
use MediaWiki\Revision\RevisionRecord;
|
||||
use MediaWiki\User\UserIdentity;
|
||||
|
@ -19,16 +19,16 @@ class MockEventDispatcher extends EventDispatcher {
|
|||
* ... expected to be a reference, value given").
|
||||
*
|
||||
* @param array &$events
|
||||
* @param ThreadItemSet $oldItemSet
|
||||
* @param ThreadItemSet $newItemSet
|
||||
* @param ContentThreadItemSet $oldItemSet
|
||||
* @param ContentThreadItemSet $newItemSet
|
||||
* @param RevisionRecord $newRevRecord
|
||||
* @param PageIdentity $title
|
||||
* @param UserIdentity $user
|
||||
*/
|
||||
public static function generateEventsFromItemSets(
|
||||
array &$events,
|
||||
ThreadItemSet $oldItemSet,
|
||||
ThreadItemSet $newItemSet,
|
||||
ContentThreadItemSet $oldItemSet,
|
||||
ContentThreadItemSet $newItemSet,
|
||||
RevisionRecord $newRevRecord,
|
||||
PageIdentity $title,
|
||||
UserIdentity $user
|
||||
|
|
Loading…
Reference in a new issue