mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/DiscussionTools
synced 2024-11-27 17:51:09 +00:00
Merge "Split off ThreadItemSet from CommentParser"
This commit is contained in:
commit
aeabff63c9
|
@ -66,6 +66,7 @@
|
|||
"modifier.js",
|
||||
"utils.js",
|
||||
"Parser.js",
|
||||
"ThreadItemSet.js",
|
||||
"ThreadItem.js",
|
||||
"CommentItem.js",
|
||||
"HeadingItem.js",
|
||||
|
|
|
@ -62,19 +62,19 @@ class ApiDiscussionToolsCompare extends ApiBase {
|
|||
return;
|
||||
}
|
||||
|
||||
$fromParser = $this->parseRevision( $fromRev );
|
||||
$toParser = $this->parseRevision( $toRev );
|
||||
$fromItemSet = $this->parseRevision( $fromRev );
|
||||
$toItemSet = $this->parseRevision( $toRev );
|
||||
|
||||
$removedComments = [];
|
||||
foreach ( $fromParser->getCommentItems() as $fromComment ) {
|
||||
if ( !$toParser->findCommentById( $fromComment->getId() ) ) {
|
||||
foreach ( $fromItemSet->getCommentItems() as $fromComment ) {
|
||||
if ( !$toItemSet->findCommentById( $fromComment->getId() ) ) {
|
||||
$removedComments[] = $fromComment->jsonSerializeForDiff();
|
||||
}
|
||||
}
|
||||
|
||||
$addedComments = [];
|
||||
foreach ( $toParser->getCommentItems() as $toComment ) {
|
||||
if ( !$fromParser->findCommentById( $toComment->getId() ) ) {
|
||||
foreach ( $toItemSet->getCommentItems() as $toComment ) {
|
||||
if ( !$fromItemSet->findCommentById( $toComment->getId() ) ) {
|
||||
$addedComments[] = $toComment->jsonSerializeForDiff();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -190,18 +190,18 @@ class ApiDiscussionToolsEdit extends ApiBase {
|
|||
|
||||
$container = DOMCompat::getBody( $doc );
|
||||
|
||||
$parser = MediaWikiServices::getInstance()->getService( 'DiscussionTools.CommentParser' )
|
||||
$threadItemSet = MediaWikiServices::getInstance()->getService( 'DiscussionTools.CommentParser' )
|
||||
->parse( $container, $title );
|
||||
|
||||
if ( $commentId ) {
|
||||
$comment = $parser->findCommentById( $commentId );
|
||||
$comment = $threadItemSet->findCommentById( $commentId );
|
||||
|
||||
if ( !$comment || !( $comment instanceof CommentItem ) ) {
|
||||
$this->dieWithError( [ 'apierror-discussiontools-commentid-notfound', $commentId ] );
|
||||
}
|
||||
|
||||
} else {
|
||||
$comments = $parser->findCommentsByName( $commentName );
|
||||
$comments = $threadItemSet->findCommentsByName( $commentName );
|
||||
$comment = $comments[ 0 ] ?? null;
|
||||
|
||||
if ( count( $comments ) > 1 ) {
|
||||
|
|
|
@ -32,8 +32,8 @@ class ApiDiscussionToolsPageInfo extends ApiBase {
|
|||
}
|
||||
|
||||
$revision = $this->getValidRevision( $title, $params['oldid'] ?? null );
|
||||
$parser = $this->parseRevision( $revision );
|
||||
$threadItems = $parser->getThreadItems();
|
||||
$threadItemSet = $this->parseRevision( $revision );
|
||||
$threadItems = $threadItemSet->getThreadItems();
|
||||
|
||||
$transcludedFrom = [];
|
||||
foreach ( $threadItems as $threadItem ) {
|
||||
|
|
|
@ -11,9 +11,9 @@ use Wikimedia\Parsoid\Utils\DOMUtils;
|
|||
trait ApiDiscussionToolsTrait {
|
||||
/**
|
||||
* @param RevisionRecord $revision
|
||||
* @return CommentParser
|
||||
* @return ThreadItemSet
|
||||
*/
|
||||
protected function parseRevision( RevisionRecord $revision ): CommentParser {
|
||||
protected function parseRevision( RevisionRecord $revision ): ThreadItemSet {
|
||||
$response = $this->requestRestbasePageHtml( $revision );
|
||||
|
||||
$doc = DOMUtils::parseHTML( $response['body'] );
|
||||
|
|
|
@ -87,8 +87,8 @@ class CommentFormatter {
|
|||
$doc = DOMUtils::parseHTML( $html );
|
||||
$container = DOMCompat::getBody( $doc );
|
||||
|
||||
$parser = static::getParser()->parse( $container, $title );
|
||||
$threadItems = $parser->getThreadItems();
|
||||
$threadItemSet = static::getParser()->parse( $container, $title );
|
||||
$threadItems = $threadItemSet->getThreadItems();
|
||||
|
||||
// Iterate in reverse order, because adding the range markers for a thread item
|
||||
// can invalidate the ranges of subsequent thread items (T298096)
|
||||
|
|
|
@ -27,17 +27,6 @@ class CommentParser {
|
|||
/** @var Title */
|
||||
private $title;
|
||||
|
||||
/** @var ThreadItem[] */
|
||||
private $threadItems;
|
||||
/** @var CommentItem[] */
|
||||
private $commentItems;
|
||||
/** @var ThreadItem[][] */
|
||||
private $threadItemsByName;
|
||||
/** @var ThreadItem[] */
|
||||
private $threadItemsById;
|
||||
/** @var HeadingItem[] */
|
||||
private $threads;
|
||||
|
||||
/** @var Config */
|
||||
private $config;
|
||||
|
||||
|
@ -75,14 +64,17 @@ class CommentParser {
|
|||
*
|
||||
* @param Element $rootNode Root node of content to parse
|
||||
* @param Title $title Title of the page being parsed
|
||||
* @return $this
|
||||
* @return ThreadItemSet
|
||||
*/
|
||||
public function parse( Element $rootNode, Title $title ) {
|
||||
public function parse( Element $rootNode, Title $title ): ThreadItemSet {
|
||||
$this->rootNode = $rootNode;
|
||||
$this->title = $title;
|
||||
// TODO Return a data object
|
||||
// (This line is a big fat hack)
|
||||
return clone $this;
|
||||
|
||||
$result = $this->buildThreadItems();
|
||||
$this->buildThreads( $result );
|
||||
$this->computeIdsAndNames( $result );
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -729,89 +721,6 @@ class CommentParser {
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all discussion comments (and headings) within a DOM subtree.
|
||||
*
|
||||
* This returns a flat list, use getThreads() to get a tree structure starting at section headings.
|
||||
*
|
||||
* For example, for a MediaWiki discussion like this (we're dealing with HTML DOM here,
|
||||
* the wikitext syntax is just for illustration):
|
||||
*
|
||||
* == A ==
|
||||
* B. ~~~~
|
||||
* : C.
|
||||
* : C. ~~~~
|
||||
* :: D. ~~~~
|
||||
* ::: E. ~~~~
|
||||
* ::: F. ~~~~
|
||||
* : G. ~~~~
|
||||
* H. ~~~~
|
||||
* : I. ~~~~
|
||||
*
|
||||
* This function would return a structure like:
|
||||
*
|
||||
* [
|
||||
* HeadingItem( { level: 0, range: (h2: A) } ),
|
||||
* CommentItem( { level: 1, range: (p: B) } ),
|
||||
* CommentItem( { level: 2, range: (li: C, li: C) } ),
|
||||
* CommentItem( { level: 3, range: (li: D) } ),
|
||||
* CommentItem( { level: 4, range: (li: E) } ),
|
||||
* CommentItem( { level: 4, range: (li: F) } ),
|
||||
* CommentItem( { level: 2, range: (li: G) } ),
|
||||
* CommentItem( { level: 1, range: (p: H) } ),
|
||||
* CommentItem( { level: 2, range: (li: I) } )
|
||||
* ]
|
||||
*
|
||||
* @return ThreadItem[] Thread items
|
||||
*/
|
||||
public function getThreadItems(): array {
|
||||
if ( !$this->threadItems ) {
|
||||
$this->buildThreads();
|
||||
}
|
||||
return $this->threadItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as getFlatThreadItems, but only returns the CommentItems
|
||||
*
|
||||
* @return CommentItem[] Comment items
|
||||
*/
|
||||
public function getCommentItems(): array {
|
||||
if ( !$this->commentItems ) {
|
||||
$this->buildThreads();
|
||||
}
|
||||
return $this->commentItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find ThreadItems by their name
|
||||
*
|
||||
* This will usually return a single-element array, but it may return multiple comments if they're
|
||||
* indistinguishable by name. In that case, use their IDs to disambiguate.
|
||||
*
|
||||
* @param string $name Name
|
||||
* @return ThreadItem[] Thread items, empty array if not found
|
||||
*/
|
||||
public function findCommentsByName( string $name ): array {
|
||||
if ( !$this->threadItemsByName ) {
|
||||
$this->buildThreads();
|
||||
}
|
||||
return $this->threadItemsByName[$name] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a ThreadItem by its ID
|
||||
*
|
||||
* @param string $id ID
|
||||
* @return ThreadItem|null Thread item, null if not found
|
||||
*/
|
||||
public function findCommentById( string $id ): ?ThreadItem {
|
||||
if ( !$this->threadItemsById ) {
|
||||
$this->buildThreads();
|
||||
}
|
||||
return $this->threadItemsById[$id] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node[] $sigNodes
|
||||
* @param array $match
|
||||
|
@ -838,19 +747,13 @@ class CommentParser {
|
|||
return $sigRange;
|
||||
}
|
||||
|
||||
private function buildThreadItems(): void {
|
||||
private function buildThreadItems(): ThreadItemSet {
|
||||
$result = new ThreadItemSet();
|
||||
|
||||
$timestampRegexps = $this->getLocalTimestampRegexps();
|
||||
$commentItems = [];
|
||||
$threadItems = [];
|
||||
$dfParsers = $this->getLocalTimestampParsers();
|
||||
|
||||
// Placeholder heading in case there are comments in the 0th section
|
||||
$range = new ImmutableRange( $this->rootNode, 0, $this->rootNode, 0 );
|
||||
$fakeHeading = new HeadingItem( $range, 99, true );
|
||||
$fakeHeading->setRootNode( $this->rootNode );
|
||||
|
||||
$curComment = $fakeHeading;
|
||||
$curCommentEnd = $range->endContainer;
|
||||
$curCommentEnd = $this->rootNode;
|
||||
|
||||
$treeWalker = new TreeWalker(
|
||||
$this->rootNode,
|
||||
|
@ -868,7 +771,7 @@ class CommentParser {
|
|||
);
|
||||
$curComment = new HeadingItem( $range, (int)( $match[ 1 ] ) );
|
||||
$curComment->setRootNode( $this->rootNode );
|
||||
$threadItems[] = $curComment;
|
||||
$result->addThreadItem( $curComment );
|
||||
$curCommentEnd = $node;
|
||||
} elseif ( $node instanceof Text && ( $match = $this->findTimestamp( $node, $timestampRegexps ) ) ) {
|
||||
$warnings = [];
|
||||
|
@ -965,70 +868,20 @@ class CommentParser {
|
|||
if ( $warnings ) {
|
||||
$curComment->addWarnings( $warnings );
|
||||
}
|
||||
$commentItems[] = $curComment;
|
||||
$threadItems[] = $curComment;
|
||||
if ( $result->isEmpty() ) {
|
||||
// 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, 99, true );
|
||||
$fakeHeading->setRootNode( $this->rootNode );
|
||||
$result->addThreadItem( $fakeHeading );
|
||||
}
|
||||
$result->addThreadItem( $curComment );
|
||||
$curCommentEnd = $curComment->getRange()->endContainer;
|
||||
}
|
||||
}
|
||||
|
||||
// Insert the fake placeholder heading if there are any comments in the 0th section
|
||||
// (before the first real heading)
|
||||
if ( count( $threadItems ) && !( $threadItems[ 0 ] instanceof HeadingItem ) ) {
|
||||
array_unshift( $threadItems, $fakeHeading );
|
||||
}
|
||||
|
||||
$this->commentItems = $commentItems;
|
||||
$this->threadItems = $threadItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group discussion comments into threads and associate replies to original messages.
|
||||
*
|
||||
* Each thread must begin with a heading. Original messages in the thread are treated as replies to
|
||||
* its heading. Other replies are associated based on the order and indentation level.
|
||||
*
|
||||
* Note that the objects in `comments` are extended in-place with the additional data.
|
||||
*
|
||||
* For example, for a MediaWiki discussion like this (we're dealing with HTML DOM here,
|
||||
* the wikitext syntax is just for illustration):
|
||||
*
|
||||
* == A ==
|
||||
* B. ~~~~
|
||||
* : C.
|
||||
* : C. ~~~~
|
||||
* :: D. ~~~~
|
||||
* ::: E. ~~~~
|
||||
* ::: F. ~~~~
|
||||
* : G. ~~~~
|
||||
* H. ~~~~
|
||||
* : I. ~~~~
|
||||
*
|
||||
* This function would return a structure like:
|
||||
*
|
||||
* [
|
||||
* HeadingItem( { level: 0, range: (h2: A), replies: [
|
||||
* CommentItem( { level: 1, range: (p: B), replies: [
|
||||
* CommentItem( { level: 2, range: (li: C, li: C), replies: [
|
||||
* CommentItem( { level: 3, range: (li: D), replies: [
|
||||
* CommentItem( { level: 4, range: (li: E), replies: [] } ),
|
||||
* CommentItem( { level: 4, range: (li: F), replies: [] } ),
|
||||
* ] } ),
|
||||
* ] } ),
|
||||
* CommentItem( { level: 2, range: (li: G), replies: [] } ),
|
||||
* ] } ),
|
||||
* CommentItem( { level: 1, range: (p: H), replies: [
|
||||
* CommentItem( { level: 2, range: (li: I), replies: [] } ),
|
||||
* ] } ),
|
||||
* ] } )
|
||||
* ]
|
||||
*
|
||||
* @return HeadingItem[] Tree structure of comments, top-level items are the headings.
|
||||
*/
|
||||
public function getThreads(): array {
|
||||
if ( !$this->threads ) {
|
||||
$this->buildThreads();
|
||||
}
|
||||
return $this->threads;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1045,9 +898,10 @@ class CommentParser {
|
|||
* Given a thread item, return an identifier for it that is unique within the page.
|
||||
*
|
||||
* @param ThreadItem $threadItem
|
||||
* @param ThreadItemSet $previousItems
|
||||
* @return string
|
||||
*/
|
||||
private function computeId( ThreadItem $threadItem ): string {
|
||||
private function computeId( ThreadItem $threadItem, ThreadItemSet $previousItems ): string {
|
||||
// When changing the algorithm below, copy the old version into computeLegacyId()
|
||||
// for compatibility with cached data.
|
||||
|
||||
|
@ -1088,12 +942,12 @@ class CommentParser {
|
|||
}
|
||||
}
|
||||
|
||||
if ( isset( $this->threadItemsById[$id] ) ) {
|
||||
if ( $previousItems->findCommentById( $id ) ) {
|
||||
// Well, that's tough
|
||||
$threadItem->addWarning( 'Duplicate comment ID' );
|
||||
// Finally, disambiguate by adding sequential numbers, to allow replying to both comments
|
||||
$number = 1;
|
||||
while ( isset( $this->threadItemsById["$id-$number"] ) ) {
|
||||
while ( $previousItems->findCommentById( "$id-$number" ) ) {
|
||||
$number++;
|
||||
}
|
||||
$id = "$id-$number";
|
||||
|
@ -1107,9 +961,10 @@ class CommentParser {
|
|||
* older algorithm, so that we can still match IDs from cached data.
|
||||
*
|
||||
* @param ThreadItem $threadItem
|
||||
* @param ThreadItemSet $previousItems
|
||||
* @return string|null
|
||||
*/
|
||||
private function computeLegacyId( ThreadItem $threadItem ): ?string {
|
||||
private function computeLegacyId( ThreadItem $threadItem, ThreadItemSet $previousItems ): ?string {
|
||||
// When we change the algorithm in computeId(), the old version should be copied below
|
||||
// for compatibility with cached data.
|
||||
|
||||
|
@ -1147,17 +1002,14 @@ class CommentParser {
|
|||
return $name;
|
||||
}
|
||||
|
||||
private function buildThreads(): void {
|
||||
if ( !$this->threadItems ) {
|
||||
$this->buildThreadItems();
|
||||
}
|
||||
|
||||
$threads = [];
|
||||
/**
|
||||
* @param ThreadItemSet $result
|
||||
*/
|
||||
private function buildThreads( ThreadItemSet $result ): void {
|
||||
$lastHeading = null;
|
||||
$replies = [];
|
||||
$this->threadItemsById = [];
|
||||
$this->threadItemsByName = [];
|
||||
|
||||
foreach ( $this->threadItems as $threadItem ) {
|
||||
foreach ( $result->getThreadItems() as $threadItem ) {
|
||||
if ( count( $replies ) < $threadItem->getLevel() ) {
|
||||
// Someone skipped an indentation level (or several). Pretend that the previous reply
|
||||
// covers multiple indentation levels, so that following comments get connected to it.
|
||||
|
@ -1169,10 +1021,9 @@ class CommentParser {
|
|||
|
||||
if ( $threadItem instanceof HeadingItem ) {
|
||||
// New root (thread)
|
||||
$threads[] = $threadItem;
|
||||
// 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.
|
||||
$maybeParent = count( $threads ) > 1 ? $threads[ count( $threads ) - 2 ] : null;
|
||||
$maybeParent = $lastHeading;
|
||||
while ( $maybeParent && $maybeParent->getHeadingLevel() >= $threadItem->getHeadingLevel() ) {
|
||||
$maybeParent = $maybeParent->getParent();
|
||||
}
|
||||
|
@ -1180,6 +1031,7 @@ class CommentParser {
|
|||
$threadItem->setParent( $maybeParent );
|
||||
$maybeParent->addReply( $threadItem );
|
||||
}
|
||||
$lastHeading = $threadItem;
|
||||
} elseif ( isset( $replies[ $threadItem->getLevel() - 1 ] ) ) {
|
||||
// Add as a reply to the closest less-nested comment
|
||||
$threadItem->setParent( $replies[ $threadItem->getLevel() - 1 ] );
|
||||
|
@ -1192,25 +1044,26 @@ class CommentParser {
|
|||
// Cut off more deeply nested replies
|
||||
array_splice( $replies, $threadItem->getLevel() + 1 );
|
||||
}
|
||||
}
|
||||
|
||||
$this->threads = $threads;
|
||||
|
||||
foreach ( $this->threadItems as $threadItem ) {
|
||||
/**
|
||||
* Set the IDs and names used to refer to comments and headings.
|
||||
* This has to be a separate pass because we don't have the list of replies before
|
||||
* this point.
|
||||
*
|
||||
* @param ThreadItemSet $result
|
||||
*/
|
||||
private function computeIdsAndNames( ThreadItemSet $result ): void {
|
||||
foreach ( $result->getThreadItems() as $threadItem ) {
|
||||
$name = $this->computeName( $threadItem );
|
||||
$threadItem->setName( $name );
|
||||
$this->threadItemsByName[$name][] = $threadItem;
|
||||
|
||||
// Set the IDs used to refer to comments and headings.
|
||||
// This has to be a separate pass because we don't have the list of replies before
|
||||
// this point.
|
||||
$id = $this->computeId( $threadItem );
|
||||
$id = $this->computeId( $threadItem, $result );
|
||||
$threadItem->setId( $id );
|
||||
$this->threadItemsById[$id] = $threadItem;
|
||||
$legacyId = $this->computeLegacyId( $threadItem );
|
||||
$legacyId = $this->computeLegacyId( $threadItem, $result );
|
||||
$threadItem->setLegacyId( $legacyId );
|
||||
if ( $legacyId ) {
|
||||
$this->threadItemsById[$legacyId] = $threadItem;
|
||||
}
|
||||
|
||||
$result->updateIdAndNameMaps( $threadItem );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,12 +19,12 @@ use ExtensionRegistry;
|
|||
use IDBAccessObject;
|
||||
use Iterator;
|
||||
use MediaWiki\Extension\DiscussionTools\CommentItem;
|
||||
use MediaWiki\Extension\DiscussionTools\CommentParser;
|
||||
use MediaWiki\Extension\DiscussionTools\HeadingItem;
|
||||
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\MediaWikiServices;
|
||||
use MediaWiki\Page\PageIdentity;
|
||||
use MediaWiki\Revision\RevisionRecord;
|
||||
|
@ -39,9 +39,9 @@ use Wikimedia\Parsoid\Utils\DOMUtils;
|
|||
class EventDispatcher {
|
||||
/**
|
||||
* @param RevisionRecord $revRecord
|
||||
* @return CommentParser
|
||||
* @return ThreadItemSet
|
||||
*/
|
||||
private static function getParsedRevision( RevisionRecord $revRecord ): CommentParser {
|
||||
private static function getParsedRevision( RevisionRecord $revRecord ): ThreadItemSet {
|
||||
$services = MediaWikiServices::getInstance();
|
||||
|
||||
$pageRecord = $services->getPageStore()->getPageById( $revRecord->getPageId() ) ?:
|
||||
|
@ -100,17 +100,17 @@ class EventDispatcher {
|
|||
}
|
||||
|
||||
if ( $oldRevRecord !== null ) {
|
||||
$oldParser = self::getParsedRevision( $oldRevRecord );
|
||||
$oldItemSet = self::getParsedRevision( $oldRevRecord );
|
||||
} else {
|
||||
// Page creation
|
||||
$doc = DOMUtils::parseHTML( '' );
|
||||
$container = DOMCompat::getBody( $doc );
|
||||
$oldParser = $services->getService( 'DiscussionTools.CommentParser' )
|
||||
$oldItemSet = $services->getService( 'DiscussionTools.CommentParser' )
|
||||
->parse( $container, $title );
|
||||
}
|
||||
$newParser = self::getParsedRevision( $newRevRecord );
|
||||
$newItemSet = self::getParsedRevision( $newRevRecord );
|
||||
|
||||
self::generateEventsFromParsers( $events, $oldParser, $newParser, $newRevRecord, $title, $user );
|
||||
self::generateEventsFromItemSets( $events, $oldItemSet, $newItemSet, $newRevRecord, $title, $user );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -143,22 +143,22 @@ class EventDispatcher {
|
|||
* Helper for generateEventsForRevision(), separated out for easier testing.
|
||||
*
|
||||
* @param array &$events
|
||||
* @param CommentParser $oldParser
|
||||
* @param CommentParser $newParser
|
||||
* @param ThreadItemSet $oldItemSet
|
||||
* @param ThreadItemSet $newItemSet
|
||||
* @param RevisionRecord $newRevRecord
|
||||
* @param PageIdentity $title
|
||||
* @param UserIdentity $user
|
||||
*/
|
||||
protected static function generateEventsFromParsers(
|
||||
protected static function generateEventsFromItemSets(
|
||||
array &$events,
|
||||
CommentParser $oldParser,
|
||||
CommentParser $newParser,
|
||||
ThreadItemSet $oldItemSet,
|
||||
ThreadItemSet $newItemSet,
|
||||
RevisionRecord $newRevRecord,
|
||||
PageIdentity $title,
|
||||
UserIdentity $user
|
||||
): void {
|
||||
$newComments = self::groupCommentsByThreadAndName( $newParser->getThreadItems() );
|
||||
$oldComments = self::groupCommentsByThreadAndName( $oldParser->getThreadItems() );
|
||||
$newComments = self::groupCommentsByThreadAndName( $newItemSet->getThreadItems() );
|
||||
$oldComments = self::groupCommentsByThreadAndName( $oldItemSet->getThreadItems() );
|
||||
$addedComments = [];
|
||||
|
||||
foreach ( $newComments as $threadName => $threadNewComments ) {
|
||||
|
|
176
includes/ThreadItemSet.php
Normal file
176
includes/ThreadItemSet.php
Normal file
|
@ -0,0 +1,176 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Extension\DiscussionTools;
|
||||
|
||||
/**
|
||||
* 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 = [];
|
||||
|
||||
/**
|
||||
* @internal Only used by CommentParser
|
||||
* @param ThreadItem $item
|
||||
*/
|
||||
public function addThreadItem( ThreadItem $item ) {
|
||||
$this->threadItems[] = $item;
|
||||
if ( $item instanceof CommentItem ) {
|
||||
$this->commentItems[] = $item;
|
||||
}
|
||||
if ( $item instanceof HeadingItem ) {
|
||||
$this->threads[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Only used by CommentParser
|
||||
* @return bool
|
||||
*/
|
||||
public function isEmpty(): bool {
|
||||
return !$this->threadItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Only used by CommentParser
|
||||
* @param ThreadItem $item
|
||||
*/
|
||||
public function updateIdAndNameMaps( ThreadItem $item ) {
|
||||
$this->threadItemsByName[ $item->getName() ][] = $item;
|
||||
|
||||
$this->threadItemsById[ $item->getId() ] = $item;
|
||||
|
||||
$legacyId = $item->getLegacyId();
|
||||
if ( $legacyId ) {
|
||||
$this->threadItemsById[ $legacyId ] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all discussion comments (and headings) within a DOM subtree.
|
||||
*
|
||||
* This returns a flat list, use getThreads() to get a tree structure starting at section headings.
|
||||
*
|
||||
* For example, for a MediaWiki discussion like this (we're dealing with HTML DOM here,
|
||||
* the wikitext syntax is just for illustration):
|
||||
*
|
||||
* == A ==
|
||||
* B. ~~~~
|
||||
* : C.
|
||||
* : C. ~~~~
|
||||
* :: D. ~~~~
|
||||
* ::: E. ~~~~
|
||||
* ::: F. ~~~~
|
||||
* : G. ~~~~
|
||||
* H. ~~~~
|
||||
* : I. ~~~~
|
||||
*
|
||||
* This function would return a structure like:
|
||||
*
|
||||
* [
|
||||
* HeadingItem( { level: 0, range: (h2: A) } ),
|
||||
* CommentItem( { level: 1, range: (p: B) } ),
|
||||
* CommentItem( { level: 2, range: (li: C, li: C) } ),
|
||||
* CommentItem( { level: 3, range: (li: D) } ),
|
||||
* CommentItem( { level: 4, range: (li: E) } ),
|
||||
* CommentItem( { level: 4, range: (li: F) } ),
|
||||
* CommentItem( { level: 2, range: (li: G) } ),
|
||||
* CommentItem( { level: 1, range: (p: H) } ),
|
||||
* CommentItem( { level: 2, range: (li: I) } )
|
||||
* ]
|
||||
*
|
||||
* @return ThreadItem[] Thread items
|
||||
*/
|
||||
public function getThreadItems(): array {
|
||||
return $this->threadItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as getFlatThreadItems, but only returns the CommentItems
|
||||
*
|
||||
* @return CommentItem[] Comment items
|
||||
*/
|
||||
public function getCommentItems(): array {
|
||||
return $this->commentItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find ThreadItems by their name
|
||||
*
|
||||
* This will usually return a single-element array, but it may return multiple comments if they're
|
||||
* indistinguishable by name. In that case, use their IDs to disambiguate.
|
||||
*
|
||||
* @param string $name Name
|
||||
* @return ThreadItem[] Thread items, empty array if not found
|
||||
*/
|
||||
public function findCommentsByName( string $name ): array {
|
||||
return $this->threadItemsByName[$name] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a ThreadItem by its ID
|
||||
*
|
||||
* @param string $id ID
|
||||
* @return ThreadItem|null Thread item, null if not found
|
||||
*/
|
||||
public function findCommentById( string $id ): ?ThreadItem {
|
||||
return $this->threadItemsById[$id] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group discussion comments into threads and associate replies to original messages.
|
||||
*
|
||||
* Each thread must begin with a heading. Original messages in the thread are treated as replies to
|
||||
* its heading. Other replies are associated based on the order and indentation level.
|
||||
*
|
||||
* Note that the objects in `comments` are extended in-place with the additional data.
|
||||
*
|
||||
* For example, for a MediaWiki discussion like this (we're dealing with HTML DOM here,
|
||||
* the wikitext syntax is just for illustration):
|
||||
*
|
||||
* == A ==
|
||||
* B. ~~~~
|
||||
* : C.
|
||||
* : C. ~~~~
|
||||
* :: D. ~~~~
|
||||
* ::: E. ~~~~
|
||||
* ::: F. ~~~~
|
||||
* : G. ~~~~
|
||||
* H. ~~~~
|
||||
* : I. ~~~~
|
||||
*
|
||||
* This function would return a structure like:
|
||||
*
|
||||
* [
|
||||
* HeadingItem( { level: 0, range: (h2: A), replies: [
|
||||
* CommentItem( { level: 1, range: (p: B), replies: [
|
||||
* CommentItem( { level: 2, range: (li: C, li: C), replies: [
|
||||
* CommentItem( { level: 3, range: (li: D), replies: [
|
||||
* CommentItem( { level: 4, range: (li: E), replies: [] } ),
|
||||
* CommentItem( { level: 4, range: (li: F), replies: [] } ),
|
||||
* ] } ),
|
||||
* ] } ),
|
||||
* CommentItem( { level: 2, range: (li: G), replies: [] } ),
|
||||
* ] } ),
|
||||
* CommentItem( { level: 1, range: (p: H), replies: [
|
||||
* CommentItem( { level: 2, range: (li: I), replies: [] } ),
|
||||
* ] } ),
|
||||
* ] } )
|
||||
* ]
|
||||
*
|
||||
* @return HeadingItem[] Tree structure of comments, top-level items are the headings.
|
||||
*/
|
||||
public function getThreads(): array {
|
||||
return $this->threads;
|
||||
}
|
||||
|
||||
}
|
|
@ -22,6 +22,7 @@
|
|||
"DmMWPingNode": "DmMWPingNode",
|
||||
"HeadingItem": "HeadingItem",
|
||||
"moment": "moment",
|
||||
"ThreadItemSet": "ThreadItemSet",
|
||||
"ThreadItem": "ThreadItem"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,15 +38,15 @@ if ( defaultVisual || enable2017Wikitext ) {
|
|||
*
|
||||
* @param {jQuery} $pageContainer Page container
|
||||
* @param {ThreadItem} threadItem Thread item to attach new comment to
|
||||
* @param {mw.dt.Parser} parser Comment parser
|
||||
* @param {ThreadItemSet} threadItemSet
|
||||
*/
|
||||
function CommentController( $pageContainer, threadItem, parser ) {
|
||||
function CommentController( $pageContainer, threadItem, threadItemSet ) {
|
||||
// Mixin constructors
|
||||
OO.EventEmitter.call( this );
|
||||
|
||||
this.$pageContainer = $pageContainer;
|
||||
this.threadItem = threadItem;
|
||||
this.parser = parser;
|
||||
this.threadItemSet = threadItemSet;
|
||||
this.newListItem = null;
|
||||
this.replyWidgetPromise = null;
|
||||
}
|
||||
|
@ -275,7 +275,7 @@ CommentController.prototype.teardown = function ( mode ) {
|
|||
CommentController.prototype.getApiQuery = function ( pageName, checkboxes ) {
|
||||
var threadItem = this.getThreadItem();
|
||||
var replyWidget = this.replyWidget;
|
||||
var sameNameComments = this.parser.findCommentsByName( threadItem.name );
|
||||
var sameNameComments = this.threadItemSet.findCommentsByName( threadItem.name );
|
||||
|
||||
var mode = replyWidget.getMode();
|
||||
var tags = [
|
||||
|
|
|
@ -9,9 +9,9 @@ var
|
|||
* Handles setup, save and teardown of new topic widget
|
||||
*
|
||||
* @param {jQuery} $pageContainer Page container
|
||||
* @param {mw.dt.Parser} parser Comment parser
|
||||
* @param {ThreadItemSet} threadItemSet
|
||||
*/
|
||||
function NewTopicController( $pageContainer, parser ) {
|
||||
function NewTopicController( $pageContainer, threadItemSet ) {
|
||||
this.container = new OO.ui.PanelLayout( {
|
||||
classes: [ 'ext-discussiontools-ui-newTopic' ],
|
||||
expanded: false,
|
||||
|
@ -45,7 +45,7 @@ function NewTopicController( $pageContainer, parser ) {
|
|||
threadItem.id = utils.NEW_TOPIC_COMMENT_ID;
|
||||
threadItem.isNewTopic = true;
|
||||
|
||||
NewTopicController.super.call( this, $pageContainer, threadItem, parser );
|
||||
NewTopicController.super.call( this, $pageContainer, threadItem, threadItemSet );
|
||||
}
|
||||
|
||||
OO.inheritClass( NewTopicController, CommentController );
|
||||
|
|
|
@ -8,6 +8,7 @@ var
|
|||
CommentItem = require( './CommentItem.js' ),
|
||||
HeadingItem = require( './HeadingItem.js' ),
|
||||
ThreadItem = require( './ThreadItem.js' ),
|
||||
ThreadItemSet = require( './ThreadItemSet.js' ),
|
||||
moment = require( './lib/moment-timezone/moment-timezone-with-data-1970-2030.js' );
|
||||
|
||||
/**
|
||||
|
@ -33,12 +34,12 @@ function Parser( data ) {
|
|||
Parser.prototype.parse = function ( rootNode, title ) {
|
||||
this.rootNode = rootNode;
|
||||
this.title = title;
|
||||
this.threadItems = null;
|
||||
this.commentItems = null;
|
||||
this.threadItemsByName = null;
|
||||
this.threadItemsById = null;
|
||||
this.threads = null;
|
||||
return this;
|
||||
|
||||
var result = this.buildThreadItems();
|
||||
this.buildThreads( result );
|
||||
this.computeIdsAndNames( result );
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
OO.initClass( Parser );
|
||||
|
@ -702,89 +703,6 @@ Parser.prototype.nextInterestingLeafNode = function ( node ) {
|
|||
return treeWalker.currentNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all discussion comments (and headings) within a DOM subtree.
|
||||
*
|
||||
* This returns a flat list, use #getThreads to get a tree structure starting at section headings.
|
||||
*
|
||||
* For example, for a MediaWiki discussion like this (we're dealing with HTML DOM here, the wikitext
|
||||
* syntax is just for illustration):
|
||||
*
|
||||
* == A ==
|
||||
* B. ~~~~
|
||||
* : C.
|
||||
* : C. ~~~~
|
||||
* :: D. ~~~~
|
||||
* ::: E. ~~~~
|
||||
* ::: F. ~~~~
|
||||
* : G. ~~~~
|
||||
* H. ~~~~
|
||||
* : I. ~~~~
|
||||
*
|
||||
* This function would return a structure like:
|
||||
*
|
||||
* [
|
||||
* HeadingItem( { level: 0, range: (h2: A) } ),
|
||||
* CommentItem( { level: 1, range: (p: B) } ),
|
||||
* CommentItem( { level: 2, range: (li: C, li: C) } ),
|
||||
* CommentItem( { level: 3, range: (li: D) } ),
|
||||
* CommentItem( { level: 4, range: (li: E) } ),
|
||||
* CommentItem( { level: 4, range: (li: F) } ),
|
||||
* CommentItem( { level: 2, range: (li: G) } ),
|
||||
* CommentItem( { level: 1, range: (p: H) } ),
|
||||
* CommentItem( { level: 2, range: (li: I) } )
|
||||
* ]
|
||||
*
|
||||
* @return {ThreadItem[]} Thread items
|
||||
*/
|
||||
Parser.prototype.getThreadItems = function () {
|
||||
if ( !this.threadItems ) {
|
||||
this.buildThreads();
|
||||
}
|
||||
return this.threadItems;
|
||||
};
|
||||
|
||||
/**
|
||||
* Same as getFlatThreadItems, but only returns the CommentItems
|
||||
*
|
||||
* @return {CommentItem[]} Comment items
|
||||
*/
|
||||
Parser.prototype.getCommentItems = function () {
|
||||
if ( !this.commentItems ) {
|
||||
this.buildThreads();
|
||||
}
|
||||
return this.commentItems;
|
||||
};
|
||||
|
||||
/**
|
||||
* Find ThreadItems by their name
|
||||
*
|
||||
* This will usually return a single-element array, but it may return multiple comments if they're
|
||||
* indistinguishable by name. In that case, use their IDs to disambiguate.
|
||||
*
|
||||
* @param {string} name Name
|
||||
* @return {ThreadItem[]} Thread items, empty array if not found
|
||||
*/
|
||||
Parser.prototype.findCommentsByName = function ( name ) {
|
||||
if ( !this.threadItemsByName ) {
|
||||
this.buildThreads();
|
||||
}
|
||||
return this.threadItemsByName[ name ] || [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Find a ThreadItem by its ID
|
||||
*
|
||||
* @param {string} id ID
|
||||
* @return {ThreadItem|null} Thread item, null if not found
|
||||
*/
|
||||
Parser.prototype.findCommentById = function ( id ) {
|
||||
if ( !this.threadItemsById ) {
|
||||
this.buildThreads();
|
||||
}
|
||||
return this.threadItemsById[ id ] || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Node[]} sigNodes
|
||||
* @param {Object} match
|
||||
|
@ -808,12 +726,15 @@ function adjustSigRange( sigNodes, match, node ) {
|
|||
return sigRange;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {ThreadItemSet}
|
||||
*/
|
||||
Parser.prototype.buildThreadItems = function () {
|
||||
var result = new ThreadItemSet();
|
||||
|
||||
var
|
||||
dfParsers = this.getLocalTimestampParsers(),
|
||||
timestampRegexps = this.getLocalTimestampRegexps(),
|
||||
commentItems = [],
|
||||
threadItems = [];
|
||||
timestampRegexps = this.getLocalTimestampRegexps();
|
||||
|
||||
var treeWalker = this.rootNode.ownerDocument.createTreeWalker(
|
||||
this.rootNode,
|
||||
|
@ -823,18 +744,8 @@ Parser.prototype.buildThreadItems = function () {
|
|||
false
|
||||
);
|
||||
|
||||
// Placeholder heading in case there are comments in the 0th section
|
||||
var range = {
|
||||
startContainer: this.rootNode,
|
||||
startOffset: 0,
|
||||
endContainer: this.rootNode,
|
||||
endOffset: 0
|
||||
};
|
||||
var fakeHeading = new HeadingItem( range, 99, true );
|
||||
fakeHeading.rootNode = this.rootNode;
|
||||
|
||||
var curComment = fakeHeading;
|
||||
var curCommentEnd = range.endContainer;
|
||||
var curComment, range;
|
||||
var curCommentEnd = this.rootNode;
|
||||
|
||||
var node, lastSigNode;
|
||||
while ( ( node = treeWalker.nextNode() ) ) {
|
||||
|
@ -851,7 +762,7 @@ Parser.prototype.buildThreadItems = function () {
|
|||
};
|
||||
curComment = new HeadingItem( range, +match[ 1 ] );
|
||||
curComment.rootNode = this.rootNode;
|
||||
threadItems.push( curComment );
|
||||
result.addThreadItem( curComment );
|
||||
curCommentEnd = node;
|
||||
} else if ( node.nodeType === Node.TEXT_NODE && ( match = this.findTimestamp( node, timestampRegexps ) ) ) {
|
||||
var warnings = [];
|
||||
|
@ -945,70 +856,25 @@ Parser.prototype.buildThreadItems = function () {
|
|||
if ( warnings.length ) {
|
||||
curComment.warnings = warnings;
|
||||
}
|
||||
commentItems.push( curComment );
|
||||
threadItems.push( curComment );
|
||||
if ( result.isEmpty() ) {
|
||||
// Add a fake placeholder heading if there are any comments in the 0th section
|
||||
// (before the first real heading)
|
||||
range = {
|
||||
startContainer: this.rootNode,
|
||||
startOffset: 0,
|
||||
endContainer: this.rootNode,
|
||||
endOffset: 0
|
||||
};
|
||||
var fakeHeading = new HeadingItem( range, 99, true );
|
||||
fakeHeading.rootNode = this.rootNode;
|
||||
result.addThreadItem( fakeHeading );
|
||||
}
|
||||
result.addThreadItem( curComment );
|
||||
curCommentEnd = curComment.range.endContainer;
|
||||
}
|
||||
}
|
||||
|
||||
// Insert the fake placeholder heading if there are any comments in the 0th section
|
||||
// (before the first real heading)
|
||||
if ( threadItems.length && !( threadItems[ 0 ] instanceof HeadingItem ) ) {
|
||||
threadItems.unshift( fakeHeading );
|
||||
}
|
||||
|
||||
this.commentItems = commentItems;
|
||||
this.threadItems = threadItems;
|
||||
};
|
||||
|
||||
/**
|
||||
* Group discussion comments into threads and associate replies to original messages.
|
||||
*
|
||||
* Each thread must begin with a heading. Original messages in the thread are treated as replies to
|
||||
* its heading. Other replies are associated based on the order and indentation level.
|
||||
*
|
||||
* Note that the objects in `comments` are extended in-place with the additional data.
|
||||
*
|
||||
* For example, for a MediaWiki discussion like this (we're dealing with HTML DOM here, the wikitext
|
||||
* syntax is just for illustration):
|
||||
*
|
||||
* == A ==
|
||||
* B. ~~~~
|
||||
* : C.
|
||||
* : C. ~~~~
|
||||
* :: D. ~~~~
|
||||
* ::: E. ~~~~
|
||||
* ::: F. ~~~~
|
||||
* : G. ~~~~
|
||||
* H. ~~~~
|
||||
* : I. ~~~~
|
||||
*
|
||||
* This function would return a structure like:
|
||||
*
|
||||
* [
|
||||
* HeadingItem( { level: 0, range: (h2: A), replies: [
|
||||
* CommentItem( { level: 1, range: (p: B), replies: [
|
||||
* CommentItem( { level: 2, range: (li: C, li: C), replies: [
|
||||
* CommentItem( { level: 3, range: (li: D), replies: [
|
||||
* CommentItem( { level: 4, range: (li: E), replies: [] } ),
|
||||
* CommentItem( { level: 4, range: (li: F), replies: [] } ),
|
||||
* ] } ),
|
||||
* ] } ),
|
||||
* CommentItem( { level: 2, range: (li: G), replies: [] } ),
|
||||
* ] } ),
|
||||
* CommentItem( { level: 1, range: (p: H), replies: [
|
||||
* CommentItem( { level: 2, range: (li: I), replies: [] } ),
|
||||
* ] } ),
|
||||
* ] } )
|
||||
* ]
|
||||
*
|
||||
* @return {HeadingItem[]} Tree structure of comments, top-level items are the headings.
|
||||
*/
|
||||
Parser.prototype.getThreads = function () {
|
||||
if ( !this.threads ) {
|
||||
this.buildThreads();
|
||||
}
|
||||
return this.threads;
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -1025,9 +891,10 @@ Parser.prototype.truncateForId = function ( text ) {
|
|||
* Given a thread item, return an identifier for it that is unique within the page.
|
||||
*
|
||||
* @param {ThreadItem} threadItem
|
||||
* @param {ThreadItemSet} previousItems
|
||||
* @return {string}
|
||||
*/
|
||||
Parser.prototype.computeId = function ( threadItem ) {
|
||||
Parser.prototype.computeId = function ( threadItem, previousItems ) {
|
||||
var id, headline;
|
||||
|
||||
if ( threadItem instanceof HeadingItem && threadItem.placeholderHeading ) {
|
||||
|
@ -1063,12 +930,12 @@ Parser.prototype.computeId = function ( threadItem ) {
|
|||
}
|
||||
}
|
||||
|
||||
if ( this.threadItemsById[ id ] ) {
|
||||
if ( previousItems.findCommentById( id ) ) {
|
||||
// Well, that's tough
|
||||
threadItem.warnings.push( 'Duplicate comment ID' );
|
||||
// Finally, disambiguate by adding sequential numbers, to allow replying to both comments
|
||||
var number = 1;
|
||||
while ( this.threadItemsById[ id + '-' + number ] ) {
|
||||
while ( previousItems.findCommentById( id + '-' + number ) ) {
|
||||
number++;
|
||||
}
|
||||
id = id + '-' + number;
|
||||
|
@ -1107,22 +974,16 @@ Parser.prototype.computeName = function ( threadItem ) {
|
|||
return name;
|
||||
};
|
||||
|
||||
Parser.prototype.buildThreads = function () {
|
||||
var
|
||||
threads = [],
|
||||
replies = [];
|
||||
|
||||
if ( !this.threadItems ) {
|
||||
this.buildThreadItems();
|
||||
}
|
||||
|
||||
this.threadItemsById = {};
|
||||
this.threadItemsByName = {};
|
||||
/**
|
||||
* @param {ThreadItemSet} result
|
||||
*/
|
||||
Parser.prototype.buildThreads = function ( result ) {
|
||||
var lastHeading = null;
|
||||
var replies = [];
|
||||
|
||||
var i, threadItem;
|
||||
|
||||
for ( i = 0; i < this.threadItems.length; i++ ) {
|
||||
threadItem = this.threadItems[ i ];
|
||||
for ( i = 0; i < result.threadItems.length; i++ ) {
|
||||
threadItem = result.threadItems[ i ];
|
||||
|
||||
if ( replies.length < threadItem.level ) {
|
||||
// Someone skipped an indentation level (or several). Pretend that the previous reply
|
||||
|
@ -1135,10 +996,9 @@ Parser.prototype.buildThreads = function () {
|
|||
|
||||
if ( threadItem instanceof HeadingItem ) {
|
||||
// New root (thread)
|
||||
threads.push( threadItem );
|
||||
// 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.
|
||||
var maybeParent = threads.length > 1 ? threads[ threads.length - 2 ] : null;
|
||||
var maybeParent = lastHeading;
|
||||
while ( maybeParent && maybeParent.headingLevel >= threadItem.headingLevel ) {
|
||||
maybeParent = maybeParent.parent;
|
||||
}
|
||||
|
@ -1146,6 +1006,7 @@ Parser.prototype.buildThreads = function () {
|
|||
threadItem.parent = maybeParent;
|
||||
maybeParent.replies.push( threadItem );
|
||||
}
|
||||
lastHeading = threadItem;
|
||||
} else if ( replies[ threadItem.level - 1 ] ) {
|
||||
// Add as a reply to the closest less-nested comment
|
||||
threadItem.parent = replies[ threadItem.level - 1 ];
|
||||
|
@ -1158,25 +1019,27 @@ Parser.prototype.buildThreads = function () {
|
|||
// Cut off more deeply nested replies
|
||||
replies.length = threadItem.level + 1;
|
||||
}
|
||||
};
|
||||
|
||||
this.threads = threads;
|
||||
|
||||
for ( i = 0; i < this.threadItems.length; i++ ) {
|
||||
threadItem = this.threadItems[ i ];
|
||||
/**
|
||||
* Set the IDs and names used to refer to comments and headings.
|
||||
* This has to be a separate pass because we don't have the list of replies before
|
||||
* this point.
|
||||
*
|
||||
* @param {ThreadItemSet} result
|
||||
*/
|
||||
Parser.prototype.computeIdsAndNames = function ( result ) {
|
||||
var i, threadItem;
|
||||
for ( i = 0; i < result.threadItems.length; i++ ) {
|
||||
threadItem = result.threadItems[ i ];
|
||||
|
||||
var name = this.computeName( threadItem );
|
||||
threadItem.name = name;
|
||||
if ( !this.threadItemsByName[ name ] ) {
|
||||
this.threadItemsByName[ name ] = [];
|
||||
}
|
||||
this.threadItemsByName[ name ].push( threadItem );
|
||||
|
||||
// Set the IDs used to refer to comments and headings.
|
||||
// This has to be a separate pass because we don't have the list of replies before
|
||||
// this point.
|
||||
var id = this.computeId( threadItem );
|
||||
var id = this.computeId( threadItem, result );
|
||||
threadItem.id = id;
|
||||
this.threadItemsById[ id ] = threadItem;
|
||||
|
||||
result.updateIdAndNameMaps( threadItem );
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -28,6 +28,10 @@ function ThreadItem( type, level, range ) {
|
|||
* @member {string} Unique ID (within the page) for this comment
|
||||
*/
|
||||
this.id = null;
|
||||
/**
|
||||
* @member {string|null} Unique ID (within the page) for this comment, according to an older algorithm
|
||||
*/
|
||||
this.legacyId = null;
|
||||
/**
|
||||
* @member {ThreadItem[]} Replies to this thread item
|
||||
*/
|
||||
|
@ -47,11 +51,10 @@ OO.initClass( ThreadItem );
|
|||
* Create a new ThreadItem from a JSON serialization
|
||||
*
|
||||
* @param {string|Object} json JSON serialization or hash object
|
||||
* @param {Object.<string,ThreadItem>} commentsById Collection of comments by ID for building replies/parent pointers
|
||||
* @return {ThreadItem}
|
||||
* @throws {Error} Unknown ThreadItem type
|
||||
*/
|
||||
ThreadItem.static.newFromJSON = function ( json, commentsById ) {
|
||||
ThreadItem.static.newFromJSON = function ( json ) {
|
||||
// The page can be served from the HTTP cache (Varnish), and the JSON may be generated
|
||||
// by an older version of our PHP code. Code below must be able to handle that.
|
||||
// See ThreadItem::jsonSerialize() in PHP.
|
||||
|
@ -98,12 +101,6 @@ ThreadItem.static.newFromJSON = function ( json, commentsById ) {
|
|||
endOffset: 0
|
||||
};
|
||||
|
||||
// Setup replies/parent pointers
|
||||
item.replies = hash.replies.map( function ( id ) {
|
||||
commentsById[ id ].parent = item;
|
||||
return commentsById[ id ];
|
||||
} );
|
||||
|
||||
return item;
|
||||
};
|
||||
|
||||
|
|
231
modules/ThreadItemSet.js
Normal file
231
modules/ThreadItemSet.js
Normal file
|
@ -0,0 +1,231 @@
|
|||
var CommentItem = require( './CommentItem.js' );
|
||||
var HeadingItem = require( './HeadingItem.js' );
|
||||
var ThreadItem = require( './ThreadItem.js' );
|
||||
|
||||
/**
|
||||
* Groups thread items (headings and comments) generated by parsing a discussion page.
|
||||
*
|
||||
* @class ThreadItemSet
|
||||
*/
|
||||
function ThreadItemSet() {
|
||||
this.threadItems = [];
|
||||
this.commentItems = [];
|
||||
this.threadItemsByName = {};
|
||||
this.threadItemsById = {};
|
||||
this.threads = [];
|
||||
}
|
||||
|
||||
OO.initClass( ThreadItemSet );
|
||||
|
||||
/**
|
||||
* Created a ThreadItemSet from DOM nodes that have been annotated by the PHP CommentFormatter with
|
||||
* metadata about the thread structure.
|
||||
*
|
||||
* @param {HTMLElement} nodes
|
||||
* @param {mw.dt.Parser} parser
|
||||
* @return {ThreadItemSet}
|
||||
*/
|
||||
ThreadItemSet.static.newFromAnnotatedNodes = function ( nodes, parser ) {
|
||||
var result = new ThreadItemSet();
|
||||
|
||||
// The page can be served from the HTTP cache (Varnish), containing data-mw-comment generated
|
||||
// by an older version of our PHP code. Code below must be able to handle that.
|
||||
// See CommentFormatter::addDiscussionTools() in PHP.
|
||||
|
||||
var i, item;
|
||||
|
||||
var items = [];
|
||||
var replyIds = [];
|
||||
var itemsById = {};
|
||||
|
||||
// Create ThreadItem objects with basic data
|
||||
for ( i = 0; i < nodes.length; i++ ) {
|
||||
var hash = JSON.parse( nodes[ i ].getAttribute( 'data-mw-comment' ) );
|
||||
item = ThreadItem.static.newFromJSON( hash );
|
||||
result.addThreadItem( item );
|
||||
|
||||
// Store info for second pass
|
||||
items[ i ] = item;
|
||||
replyIds[ i ] = hash.replies;
|
||||
itemsById[ item.id ] = item;
|
||||
}
|
||||
|
||||
// Now that we have all objects, we can set up replies/parent pointers
|
||||
for ( i = 0; i < nodes.length; i++ ) {
|
||||
item = items[ i ];
|
||||
|
||||
// eslint-disable-next-line no-loop-func
|
||||
item.replies = replyIds[ i ].map( function ( id ) {
|
||||
itemsById[ id ].parent = item;
|
||||
return itemsById[ id ];
|
||||
} );
|
||||
|
||||
// Recalculate legacy IDs (and calculate names, currently not stored in the metadata)
|
||||
var newId = parser.computeId( item, result );
|
||||
if ( newId !== item.id ) {
|
||||
item.legacyId = item.id;
|
||||
item.id = newId;
|
||||
}
|
||||
item.name = parser.computeName( item );
|
||||
|
||||
result.updateIdAndNameMaps( item );
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {ThreadItem} item
|
||||
*/
|
||||
ThreadItemSet.prototype.addThreadItem = function ( item ) {
|
||||
this.threadItems.push( item );
|
||||
if ( item instanceof CommentItem ) {
|
||||
this.commentItems.push( item );
|
||||
}
|
||||
if ( item instanceof HeadingItem ) {
|
||||
this.threads.push( item );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
ThreadItemSet.prototype.isEmpty = function () {
|
||||
return this.threadItems.length === 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {ThreadItem} item
|
||||
*/
|
||||
ThreadItemSet.prototype.updateIdAndNameMaps = function ( item ) {
|
||||
if ( !this.threadItemsByName[ item.name ] ) {
|
||||
this.threadItemsByName[ item.name ] = [];
|
||||
}
|
||||
this.threadItemsByName[ item.name ].push( item );
|
||||
|
||||
this.threadItemsById[ item.id ] = item;
|
||||
|
||||
if ( item.legacyId ) {
|
||||
this.threadItemsById[ item.legacyId ] = item;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all discussion comments (and headings) within a DOM subtree.
|
||||
*
|
||||
* This returns a flat list, use #getThreads to get a tree structure starting at section headings.
|
||||
*
|
||||
* For example, for a MediaWiki discussion like this (we're dealing with HTML DOM here, the wikitext
|
||||
* syntax is just for illustration):
|
||||
*
|
||||
* == A ==
|
||||
* B. ~~~~
|
||||
* : C.
|
||||
* : C. ~~~~
|
||||
* :: D. ~~~~
|
||||
* ::: E. ~~~~
|
||||
* ::: F. ~~~~
|
||||
* : G. ~~~~
|
||||
* H. ~~~~
|
||||
* : I. ~~~~
|
||||
*
|
||||
* This function would return a structure like:
|
||||
*
|
||||
* [
|
||||
* HeadingItem( { level: 0, range: (h2: A) } ),
|
||||
* CommentItem( { level: 1, range: (p: B) } ),
|
||||
* CommentItem( { level: 2, range: (li: C, li: C) } ),
|
||||
* CommentItem( { level: 3, range: (li: D) } ),
|
||||
* CommentItem( { level: 4, range: (li: E) } ),
|
||||
* CommentItem( { level: 4, range: (li: F) } ),
|
||||
* CommentItem( { level: 2, range: (li: G) } ),
|
||||
* CommentItem( { level: 1, range: (p: H) } ),
|
||||
* CommentItem( { level: 2, range: (li: I) } )
|
||||
* ]
|
||||
*
|
||||
* @return {ThreadItem[]} Thread items
|
||||
*/
|
||||
ThreadItemSet.prototype.getThreadItems = function () {
|
||||
return this.threadItems;
|
||||
};
|
||||
|
||||
/**
|
||||
* Same as getFlatThreadItems, but only returns the CommentItems
|
||||
*
|
||||
* @return {CommentItem[]} Comment items
|
||||
*/
|
||||
ThreadItemSet.prototype.getCommentItems = function () {
|
||||
return this.commentItems;
|
||||
};
|
||||
|
||||
/**
|
||||
* Find ThreadItems by their name
|
||||
*
|
||||
* This will usually return a single-element array, but it may return multiple comments if they're
|
||||
* indistinguishable by name. In that case, use their IDs to disambiguate.
|
||||
*
|
||||
* @param {string} name Name
|
||||
* @return {ThreadItem[]} Thread items, empty array if not found
|
||||
*/
|
||||
ThreadItemSet.prototype.findCommentsByName = function ( name ) {
|
||||
return this.threadItemsByName[ name ] || [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Find a ThreadItem by its ID
|
||||
*
|
||||
* @param {string} id ID
|
||||
* @return {ThreadItem|null} Thread item, null if not found
|
||||
*/
|
||||
ThreadItemSet.prototype.findCommentById = function ( id ) {
|
||||
return this.threadItemsById[ id ] || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Group discussion comments into threads and associate replies to original messages.
|
||||
*
|
||||
* Each thread must begin with a heading. Original messages in the thread are treated as replies to
|
||||
* its heading. Other replies are associated based on the order and indentation level.
|
||||
*
|
||||
* Note that the objects in `comments` are extended in-place with the additional data.
|
||||
*
|
||||
* For example, for a MediaWiki discussion like this (we're dealing with HTML DOM here, the wikitext
|
||||
* syntax is just for illustration):
|
||||
*
|
||||
* == A ==
|
||||
* B. ~~~~
|
||||
* : C.
|
||||
* : C. ~~~~
|
||||
* :: D. ~~~~
|
||||
* ::: E. ~~~~
|
||||
* ::: F. ~~~~
|
||||
* : G. ~~~~
|
||||
* H. ~~~~
|
||||
* : I. ~~~~
|
||||
*
|
||||
* This function would return a structure like:
|
||||
*
|
||||
* [
|
||||
* HeadingItem( { level: 0, range: (h2: A), replies: [
|
||||
* CommentItem( { level: 1, range: (p: B), replies: [
|
||||
* CommentItem( { level: 2, range: (li: C, li: C), replies: [
|
||||
* CommentItem( { level: 3, range: (li: D), replies: [
|
||||
* CommentItem( { level: 4, range: (li: E), replies: [] } ),
|
||||
* CommentItem( { level: 4, range: (li: F), replies: [] } ),
|
||||
* ] } ),
|
||||
* ] } ),
|
||||
* CommentItem( { level: 2, range: (li: G), replies: [] } ),
|
||||
* ] } ),
|
||||
* CommentItem( { level: 1, range: (p: H), replies: [
|
||||
* CommentItem( { level: 2, range: (li: I), replies: [] } ),
|
||||
* ] } ),
|
||||
* ] } )
|
||||
* ]
|
||||
*
|
||||
* @return {HeadingItem[]} Tree structure of comments, top-level items are the headings.
|
||||
*/
|
||||
ThreadItemSet.prototype.getThreads = function () {
|
||||
return this.threads;
|
||||
};
|
||||
|
||||
module.exports = ThreadItemSet;
|
|
@ -8,7 +8,7 @@ var
|
|||
MemoryStorage = require( './MemoryStorage.js' ),
|
||||
storage = new MemoryStorage( mw.storage.session.store ),
|
||||
Parser = require( './Parser.js' ),
|
||||
ThreadItem = require( './ThreadItem.js' ),
|
||||
ThreadItemSet = require( './ThreadItemSet.js' ),
|
||||
CommentItem = require( './CommentItem.js' ),
|
||||
CommentDetails = require( './CommentDetails.js' ),
|
||||
ReplyLinksController = require( './ReplyLinksController.js' ),
|
||||
|
@ -516,10 +516,10 @@ var $highlightedTarget = null;
|
|||
/**
|
||||
* Highlight the comment on the page associated with the URL hash
|
||||
*
|
||||
* @param {mw.dt.Parser} parser Comment parser
|
||||
* @param {ThreadItemSet} threadItemSet
|
||||
* @param {boolean} [noScroll] Don't scroll to the topmost highlighted comment, e.g. on popstate
|
||||
*/
|
||||
function highlightTargetComment( parser, noScroll ) {
|
||||
function highlightTargetComment( threadItemSet, noScroll ) {
|
||||
// Delay with setTimeout() because "the Document's target element" (corresponding to the :target
|
||||
// selector in CSS) is not yet updated to match the URL when handling a 'popstate' event.
|
||||
setTimeout( function () {
|
||||
|
@ -539,13 +539,13 @@ function highlightTargetComment( parser, noScroll ) {
|
|||
}
|
||||
var targetIds = uri && uri.query.dtnewcomments && uri.query.dtnewcomments.split( '|' );
|
||||
if ( targetElement && targetElement.hasAttribute( 'data-mw-comment-start' ) ) {
|
||||
var comment = parser.findCommentById( targetElement.getAttribute( 'id' ) );
|
||||
var comment = threadItemSet.findCommentById( targetElement.getAttribute( 'id' ) );
|
||||
$highlightedTarget = highlight( comment );
|
||||
$highlightedTarget.addClass( 'ext-discussiontools-init-targetcomment' );
|
||||
$highlightedTarget.addClass( 'ext-discussiontools-init-highlight-fadein' );
|
||||
} else if ( targetIds ) {
|
||||
var comments = targetIds.map( function ( id ) {
|
||||
return parser.findCommentById( id );
|
||||
return threadItemSet.findCommentById( id );
|
||||
} ).filter( function ( cmt ) {
|
||||
return !!cmt;
|
||||
} );
|
||||
|
@ -576,9 +576,9 @@ function highlightTargetComment( parser, noScroll ) {
|
|||
/**
|
||||
* Clear the highlighting of the comment in the URL hash
|
||||
*
|
||||
* @param {mw.dt.Parser} parser Comment parser
|
||||
* @param {ThreadItemSet} threadItemSet
|
||||
*/
|
||||
function clearHighlightTargetComment( parser ) {
|
||||
function clearHighlightTargetComment( threadItemSet ) {
|
||||
// eslint-disable-next-line no-jquery/no-global-selector
|
||||
var targetElement = $( ':target' )[ 0 ];
|
||||
if ( targetElement && targetElement.hasAttribute( 'data-mw-comment-start' ) ) {
|
||||
|
@ -597,7 +597,7 @@ function clearHighlightTargetComment( parser ) {
|
|||
} else if ( window.location.search.match( /(^\?|&)dtnewcomments=/ ) ) {
|
||||
history.pushState( null, document.title,
|
||||
window.location.search.replace( /(^\?|&)dtnewcomments=[^&]+/, '' ) + window.location.hash );
|
||||
highlightTargetComment( parser );
|
||||
highlightTargetComment( threadItemSet );
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -614,65 +614,17 @@ function init( $container, state ) {
|
|||
activeController = null,
|
||||
// Loads later to avoid circular dependency
|
||||
CommentController = require( './CommentController.js' ),
|
||||
NewTopicController = require( './NewTopicController.js' ),
|
||||
threadItemsById = {},
|
||||
threadItems = [];
|
||||
NewTopicController = require( './NewTopicController.js' );
|
||||
|
||||
// Lazy-load postEdit module, may be required later (on desktop)
|
||||
mw.loader.using( 'mediawiki.action.view.postEdit' );
|
||||
|
||||
$pageContainer = $container;
|
||||
linksController = new ReplyLinksController( $pageContainer );
|
||||
var parser = new Parser( require( './parser/data.json' ) ).parse(
|
||||
$pageContainer[ 0 ],
|
||||
mw.Title.newFromText( mw.config.get( 'wgRelevantPageName' ) )
|
||||
);
|
||||
var parser = new Parser( require( './parser/data.json' ) );
|
||||
|
||||
var pageThreads = [];
|
||||
var commentNodes = $pageContainer[ 0 ].querySelectorAll( '[data-mw-comment]' );
|
||||
threadItems.length = commentNodes.length;
|
||||
|
||||
// The page can be served from the HTTP cache (Varnish), containing data-mw-comment generated
|
||||
// by an older version of our PHP code. Code below must be able to handle that.
|
||||
// See CommentFormatter::addDiscussionTools() in PHP.
|
||||
|
||||
// Iterate over commentNodes backwards so replies are always deserialized before their parents.
|
||||
var i, comment;
|
||||
for ( i = commentNodes.length - 1; i >= 0; i-- ) {
|
||||
var hash = JSON.parse( commentNodes[ i ].getAttribute( 'data-mw-comment' ) );
|
||||
comment = ThreadItem.static.newFromJSON( hash, threadItemsById );
|
||||
if ( !comment.name ) {
|
||||
comment.name = parser.computeName( comment );
|
||||
}
|
||||
|
||||
threadItemsById[ comment.id ] = comment;
|
||||
|
||||
if ( comment.type === 'heading' ) {
|
||||
// Use unshift as we are in a backwards loop
|
||||
pageThreads.unshift( comment );
|
||||
}
|
||||
threadItems[ i ] = comment;
|
||||
}
|
||||
|
||||
// Recalculate legacy IDs
|
||||
parser.threadItemsByName = {};
|
||||
parser.threadItemsById = {};
|
||||
// In the forward order this time, as the IDs for indistinguishable comments depend on it
|
||||
for ( i = 0; i < threadItems.length; i++ ) {
|
||||
comment = threadItems[ i ];
|
||||
|
||||
if ( !parser.threadItemsByName[ comment.name ] ) {
|
||||
parser.threadItemsByName[ comment.name ] = [];
|
||||
}
|
||||
parser.threadItemsByName[ comment.name ].push( comment );
|
||||
|
||||
var newId = parser.computeId( comment );
|
||||
parser.threadItemsById[ newId ] = comment;
|
||||
if ( newId !== comment.id ) {
|
||||
comment.id = newId;
|
||||
threadItemsById[ newId ] = comment;
|
||||
}
|
||||
}
|
||||
var result = ThreadItemSet.static.newFromAnnotatedNodes( commentNodes, parser );
|
||||
|
||||
if ( featuresEnabled.topicsubscription ) {
|
||||
initTopicSubscriptions( $container );
|
||||
|
@ -693,9 +645,9 @@ function init( $container, state ) {
|
|||
$addSectionLink = $( '#ca-addsection a' );
|
||||
// When opening new topic tool using any link, always activate the link in page tabs too
|
||||
$link = $link.add( $addSectionLink );
|
||||
commentController = new NewTopicController( $pageContainer, parser );
|
||||
commentController = new NewTopicController( $pageContainer, result );
|
||||
} else {
|
||||
commentController = new CommentController( $pageContainer, parser.findCommentById( commentId ), parser );
|
||||
commentController = new CommentController( $pageContainer, result.findCommentById( commentId ), result );
|
||||
}
|
||||
|
||||
activeCommentId = commentId;
|
||||
|
@ -749,8 +701,8 @@ function init( $container, state ) {
|
|||
// Restore autosave
|
||||
( function () {
|
||||
var mode, $link;
|
||||
for ( i = 0; i < threadItems.length; i++ ) {
|
||||
comment = threadItems[ i ];
|
||||
for ( var i = 0; i < result.threadItems.length; i++ ) {
|
||||
var comment = result.threadItems[ i ];
|
||||
if ( storage.get( 'reply/' + comment.id + '/saveable' ) ) {
|
||||
mode = storage.get( 'reply/' + comment.id + '/mode' );
|
||||
$link = $( commentNodes[ i ] );
|
||||
|
@ -767,7 +719,7 @@ function init( $container, state ) {
|
|||
}() );
|
||||
|
||||
// For debugging (now unused in the code)
|
||||
mw.dt.pageThreads = pageThreads;
|
||||
mw.dt.pageThreads = result;
|
||||
|
||||
var promise = OO.ui.isMobile() && mw.loader.getState( 'mobile.init' ) ?
|
||||
mw.loader.using( 'mobile.init' ) :
|
||||
|
@ -777,7 +729,7 @@ function init( $container, state ) {
|
|||
var $highlight;
|
||||
if ( state.repliedTo === utils.NEW_TOPIC_COMMENT_ID ) {
|
||||
// Highlight the last comment on the page
|
||||
var lastComment = threadItems[ threadItems.length - 1 ];
|
||||
var lastComment = result.threadItems[ result.threadItems.length - 1 ];
|
||||
$highlight = highlight( lastComment );
|
||||
lastHighlightComment = lastComment;
|
||||
|
||||
|
@ -798,7 +750,7 @@ function init( $container, state ) {
|
|||
|
||||
} else if ( state.repliedTo ) {
|
||||
// Find the comment we replied to, then highlight the last reply
|
||||
var repliedToComment = threadItemsById[ state.repliedTo ];
|
||||
var repliedToComment = result.threadItemsById[ state.repliedTo ];
|
||||
$highlight = highlight( repliedToComment.replies[ repliedToComment.replies.length - 1 ] );
|
||||
lastHighlightComment = repliedToComment.replies[ repliedToComment.replies.length - 1 ];
|
||||
|
||||
|
@ -850,19 +802,19 @@ function init( $container, state ) {
|
|||
if ( state.repliedTo ) {
|
||||
// Edited by using the reply tool or new topic tool. Only check the edited topic.
|
||||
if ( state.repliedTo === utils.NEW_TOPIC_COMMENT_ID ) {
|
||||
recentComments.push( threadItems[ threadItems.length - 1 ] );
|
||||
recentComments.push( result.threadItems[ result.threadItems.length - 1 ] );
|
||||
} else {
|
||||
recentComments.push( threadItemsById[ state.repliedTo ] );
|
||||
recentComments.push( result.threadItemsById[ state.repliedTo ] );
|
||||
}
|
||||
} else if ( mw.config.get( 'wgPostEdit' ) ) {
|
||||
// Edited by using wikitext editor. Check topics with their own comments within last minute.
|
||||
for ( i = 0; i < threadItems.length; i++ ) {
|
||||
for ( var i = 0; i < result.threadItems.length; i++ ) {
|
||||
if (
|
||||
threadItems[ i ] instanceof CommentItem &&
|
||||
threadItems[ i ].author === mw.user.getName() &&
|
||||
threadItems[ i ].timestamp.isSameOrAfter( moment().subtract( 1, 'minute' ), 'minute' )
|
||||
result.threadItems[ i ] instanceof CommentItem &&
|
||||
result.threadItems[ i ].author === mw.user.getName() &&
|
||||
result.threadItems[ i ].timestamp.isSameOrAfter( moment().subtract( 1, 'minute' ), 'minute' )
|
||||
) {
|
||||
recentComments.push( threadItems[ i ] );
|
||||
recentComments.push( result.threadItems[ i ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -885,15 +837,15 @@ function init( $container, state ) {
|
|||
);
|
||||
|
||||
$( window ).on( 'popstate', function () {
|
||||
highlightTargetComment( parser, true );
|
||||
highlightTargetComment( result, true );
|
||||
} );
|
||||
// eslint-disable-next-line no-jquery/no-global-selector
|
||||
$( 'body' ).on( 'click', function ( e ) {
|
||||
if ( utils.isUnmodifiedLeftClick( e ) ) {
|
||||
clearHighlightTargetComment( parser );
|
||||
clearHighlightTargetComment( result );
|
||||
}
|
||||
} );
|
||||
highlightTargetComment( parser );
|
||||
highlightTargetComment( result );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,12 +3,13 @@ var
|
|||
modifier = require( 'ext.discussionTools.init' ).modifier,
|
||||
utils = require( 'ext.discussionTools.init' ).utils,
|
||||
highlighter = require( './highlighter.js' ),
|
||||
parser = new Parser( require( 'ext.discussionTools.init' ).parserData ).parse(
|
||||
parser = new Parser( require( 'ext.discussionTools.init' ).parserData ),
|
||||
result = parser.parse(
|
||||
document.getElementById( 'mw-content-text' ),
|
||||
mw.Title.newFromText( mw.config.get( 'wgRelevantPageName' ) )
|
||||
),
|
||||
comments = parser.getCommentItems(),
|
||||
threads = parser.getThreads(),
|
||||
comments = result.getCommentItems(),
|
||||
threads = result.getThreads(),
|
||||
timestampRegexps = parser.getLocalTimestampRegexps(),
|
||||
debug = +( new mw.Uri().query.dtdebug ),
|
||||
DEBUG_HIGHLIGHT = 1,
|
||||
|
|
|
@ -36,8 +36,8 @@ class CommentModifierTest extends IntegrationTestCase {
|
|||
$doc = self::createDocument( $dom );
|
||||
$container = DOMCompat::getBody( $doc );
|
||||
|
||||
$parser = self::createParser( $data )->parse( $container, $title );
|
||||
$comments = $parser->getCommentItems();
|
||||
$threadItemSet = self::createParser( $data )->parse( $container, $title );
|
||||
$comments = $threadItemSet->getCommentItems();
|
||||
|
||||
foreach ( $comments as $comment ) {
|
||||
$node = CommentModifier::addListItem( $comment, $replyIndentation );
|
||||
|
@ -81,8 +81,8 @@ class CommentModifierTest extends IntegrationTestCase {
|
|||
$doc = self::createDocument( $dom );
|
||||
$container = DOMCompat::getBody( $doc );
|
||||
|
||||
$parser = self::createParser( $data )->parse( $container, $title );
|
||||
$comments = $parser->getCommentItems();
|
||||
$threadItemSet = self::createParser( $data )->parse( $container, $title );
|
||||
$comments = $threadItemSet->getCommentItems();
|
||||
|
||||
foreach ( $comments as $comment ) {
|
||||
$linkNode = $doc->createElement( 'a' );
|
||||
|
|
|
@ -182,7 +182,10 @@ class CommentParserTest extends IntegrationTestCase {
|
|||
|
||||
/**
|
||||
* @dataProvider provideComments
|
||||
* @covers ::getThreads
|
||||
* @covers ::parse
|
||||
* @covers ::buildThreadItems
|
||||
* @covers ::buildThreads
|
||||
* @covers ::computeIdsAndNames
|
||||
*/
|
||||
public function testGetThreads(
|
||||
string $name, string $title, string $dom, string $expected, string $config, string $data
|
||||
|
@ -198,8 +201,8 @@ class CommentParserTest extends IntegrationTestCase {
|
|||
$body = DOMCompat::getBody( $doc );
|
||||
|
||||
$this->setupEnv( $config, $data );
|
||||
$parser = self::createParser( $data )->parse( $body, $title );
|
||||
$threads = $parser->getThreads();
|
||||
$threadItemSet = self::createParser( $data )->parse( $body, $title );
|
||||
$threads = $threadItemSet->getThreads();
|
||||
|
||||
$processedThreads = [];
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ class EventDispatcherTest extends IntegrationTestCase {
|
|||
|
||||
/**
|
||||
* @dataProvider provideGenerateCases
|
||||
* @covers ::generateEventsFromParsers
|
||||
* @covers ::generateEventsFromItemSets
|
||||
*/
|
||||
public function testGenerateEventsFromParsers(
|
||||
string $rev1, string $rev2, string $authorUsername, string $other, string $expected
|
||||
|
@ -40,8 +40,9 @@ class EventDispatcherTest extends IntegrationTestCase {
|
|||
|
||||
$dummyTitle = Title::newFromText( 'Dummy' );
|
||||
$this->setupEnv( $config, $data );
|
||||
$parser1 = self::createParser( $data )->parse( $body1, $dummyTitle );
|
||||
$parser2 = self::createParser( $data )->parse( $body2, $dummyTitle );
|
||||
$parser = self::createParser( $data );
|
||||
$itemSet1 = $parser->parse( $body1, $dummyTitle );
|
||||
$itemSet2 = $parser->parse( $body2, $dummyTitle );
|
||||
|
||||
$events = self::getJson( $other, true );
|
||||
|
||||
|
@ -50,8 +51,8 @@ class EventDispatcherTest extends IntegrationTestCase {
|
|||
$fakeRevRecord = new MutableRevisionRecord( $fakeTitle );
|
||||
// All mock comments are posted between 00:00 and 00:10 on 2020-01-01
|
||||
$fakeRevRecord->setTimestamp( ( new DateTimeImmutable( '2020-01-01T00:10' ) )->format( 'c' ) );
|
||||
MockEventDispatcher::generateEventsFromParsers(
|
||||
$events, $parser1, $parser2, $fakeRevRecord, $fakeTitle, $fakeUser
|
||||
MockEventDispatcher::generateEventsFromItemSets(
|
||||
$events, $itemSet1, $itemSet2, $fakeRevRecord, $fakeTitle, $fakeUser
|
||||
);
|
||||
|
||||
foreach ( $events as &$event ) {
|
||||
|
@ -68,8 +69,8 @@ class EventDispatcherTest extends IntegrationTestCase {
|
|||
// Assert that no events are generated for comments saved >10 minutes after their timestamps
|
||||
$events = self::getJson( $other, true );
|
||||
$fakeRevRecord->setTimestamp( ( new DateTimeImmutable( '2020-01-01T00:20' ) )->format( 'c' ) );
|
||||
MockEventDispatcher::generateEventsFromParsers(
|
||||
$events, $parser1, $parser2, $fakeRevRecord, $fakeTitle, $fakeUser
|
||||
MockEventDispatcher::generateEventsFromItemSets(
|
||||
$events, $itemSet1, $itemSet2, $fakeRevRecord, $fakeTitle, $fakeUser
|
||||
);
|
||||
|
||||
foreach ( $events as &$event ) {
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
namespace MediaWiki\Extension\DiscussionTools\Tests;
|
||||
|
||||
use MediaWiki\Extension\DiscussionTools\CommentParser;
|
||||
use MediaWiki\Extension\DiscussionTools\Notifications\EventDispatcher;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItemSet;
|
||||
use MediaWiki\Page\PageIdentity;
|
||||
use MediaWiki\Revision\RevisionRecord;
|
||||
use MediaWiki\User\UserIdentity;
|
||||
|
@ -19,24 +19,24 @@ class MockEventDispatcher extends EventDispatcher {
|
|||
* ... expected to be a reference, value given").
|
||||
*
|
||||
* @param array &$events
|
||||
* @param CommentParser $oldParser
|
||||
* @param CommentParser $newParser
|
||||
* @param ThreadItemSet $oldItemSet
|
||||
* @param ThreadItemSet $newItemSet
|
||||
* @param RevisionRecord $newRevRecord
|
||||
* @param PageIdentity $title
|
||||
* @param UserIdentity $user
|
||||
*/
|
||||
public static function generateEventsFromParsers(
|
||||
public static function generateEventsFromItemSets(
|
||||
array &$events,
|
||||
CommentParser $oldParser,
|
||||
CommentParser $newParser,
|
||||
ThreadItemSet $oldItemSet,
|
||||
ThreadItemSet $newItemSet,
|
||||
RevisionRecord $newRevRecord,
|
||||
PageIdentity $title,
|
||||
UserIdentity $user
|
||||
): void {
|
||||
parent::generateEventsFromParsers(
|
||||
parent::generateEventsFromItemSets(
|
||||
$events,
|
||||
$oldParser,
|
||||
$newParser,
|
||||
$oldItemSet,
|
||||
$newItemSet,
|
||||
$newRevRecord,
|
||||
$title,
|
||||
$user
|
||||
|
|
|
@ -69,8 +69,8 @@ class ThreadItemTest extends IntegrationTestCase {
|
|||
|
||||
CommentUtils::unwrapParsoidSections( $container );
|
||||
|
||||
$parser = self::createParser( $data )->parse( $container, $title );
|
||||
$comments = $parser->getCommentItems();
|
||||
$threadItemSet = self::createParser( $data )->parse( $container, $title );
|
||||
$comments = $threadItemSet->getCommentItems();
|
||||
|
||||
$transcludedFrom = [];
|
||||
foreach ( $comments as $comment ) {
|
||||
|
@ -113,8 +113,8 @@ class ThreadItemTest extends IntegrationTestCase {
|
|||
$body = DOMCompat::getBody( $doc );
|
||||
|
||||
$this->setupEnv( $config, $data );
|
||||
$parser = self::createParser( $data )->parse( $body, $title );
|
||||
$items = $parser->getThreadItems();
|
||||
$threadItemSet = self::createParser( $data )->parse( $body, $title );
|
||||
$items = $threadItemSet->getThreadItems();
|
||||
|
||||
$output = [];
|
||||
foreach ( $items as $item ) {
|
||||
|
@ -159,8 +159,8 @@ class ThreadItemTest extends IntegrationTestCase {
|
|||
$body = DOMCompat::getBody( $doc );
|
||||
|
||||
$this->setupEnv( $config, $data );
|
||||
$parser = self::createParser( $data )->parse( $body, $title );
|
||||
$items = $parser->getThreadItems();
|
||||
$threadItemSet = self::createParser( $data )->parse( $body, $title );
|
||||
$items = $threadItemSet->getThreadItems();
|
||||
|
||||
$output = [];
|
||||
foreach ( $items as $item ) {
|
||||
|
|
|
@ -25,8 +25,8 @@ require( '../cases/modified.json' ).forEach( function ( caseItem, i ) {
|
|||
$( fixture ).empty().append( dom );
|
||||
var reverseExpectedHtml = fixture.innerHTML;
|
||||
|
||||
var parser = new Parser( data ).parse( fixture, title );
|
||||
var comments = parser.getCommentItems();
|
||||
var threadItemSet = new Parser( data ).parse( fixture, title );
|
||||
var comments = threadItemSet.getCommentItems();
|
||||
|
||||
// Add a reply to every comment. Note that this inserts *all* of the replies, unlike the real
|
||||
// thing, which only deals with one at a time. This isn't ideal but resetting everything after
|
||||
|
@ -81,8 +81,8 @@ QUnit.test( '#addReplyLink', function ( assert ) {
|
|||
|
||||
$( fixture ).empty().append( dom );
|
||||
|
||||
var parser = new Parser( data ).parse( fixture, title );
|
||||
var comments = parser.getCommentItems();
|
||||
var threadItemSet = new Parser( data ).parse( fixture, title );
|
||||
var comments = threadItemSet.getCommentItems();
|
||||
|
||||
// Add a reply link to every comment.
|
||||
comments.forEach( function ( comment ) {
|
||||
|
|
|
@ -74,8 +74,8 @@ QUnit.test( '#getThreads', function ( assert ) {
|
|||
$( fixture ).empty().append( $dom );
|
||||
testUtils.overrideMwConfig( config );
|
||||
|
||||
var parser = new Parser( data ).parse( fixture, title );
|
||||
var threads = parser.getThreads();
|
||||
var threadItemSet = new Parser( data ).parse( fixture, title );
|
||||
var threads = threadItemSet.getThreads();
|
||||
|
||||
threads.forEach( function ( thread, i ) {
|
||||
testUtils.serializeComments( thread, fixture );
|
||||
|
|
|
@ -72,6 +72,8 @@ module.exports.serializeComments = function ( parent, root ) {
|
|||
// Unimportant
|
||||
delete parent.rootNode;
|
||||
|
||||
delete parent.legacyId;
|
||||
|
||||
parent.replies.forEach( function ( comment ) {
|
||||
module.exports.serializeComments( comment, root );
|
||||
} );
|
||||
|
|
Loading…
Reference in a new issue