diff --git a/includes/CommentFormatter.php b/includes/CommentFormatter.php index 533980985..ce6dd156e 100644 --- a/includes/CommentFormatter.php +++ b/includes/CommentFormatter.php @@ -149,20 +149,20 @@ class CommentFormatter { } // Visual enhancements: topic containers - $summary = $headingItem->getThreadSummary(); - if ( $summary['commentCount'] ) { - $latestReplyJSON = static::getJsonForCommentMarker( $summary['latestReply'] ); + $latestReplyItem = $headingItem->getLatestReply(); + if ( $latestReplyItem ) { + $latestReplyJSON = static::getJsonForCommentMarker( $latestReplyItem ); $latestReply = $doc->createComment( // Timestamp output varies by user timezone, so is formatted later '__DTLATESTCOMMENTTHREAD__' . htmlspecialchars( $latestReplyJSON, ENT_NOQUOTES ) . '__' ); $commentCount = $doc->createComment( - '__DTCOMMENTCOUNT__' . $summary['commentCount'] . '__' + '__DTCOMMENTCOUNT__' . $headingItem->getCommentCount() . '__' ); $authorCount = $doc->createComment( - '__DTAUTHORCOUNT__' . count( $summary['authors'] ) . '__' + '__DTAUTHORCOUNT__' . count( $headingItem->getAuthorsBelow() ) . '__' ); // Topic subscriptions @@ -201,7 +201,9 @@ class CommentFormatter { $headingElement->appendChild( $bar ); } - $tocInfo[ $headingItem->getLinkableTitle() ] = $summary; + $tocInfo[ $headingItem->getLinkableTitle() ] = [ + 'commentCount' => $headingItem->getCommentCount(), + ]; } /** diff --git a/includes/CommentParser.php b/includes/CommentParser.php index be82b095f..7f6969b0d 100644 --- a/includes/CommentParser.php +++ b/includes/CommentParser.php @@ -979,7 +979,7 @@ class CommentParser { // (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 // heading ID. - $oldestComment = $threadItem->getThreadSummary()['oldestReply']; + $oldestComment = $threadItem->getOldestReply(); if ( $oldestComment ) { $id .= '-' . $oldestComment->getTimestampString(); } @@ -1013,7 +1013,7 @@ class CommentParser { if ( $threadItem instanceof ContentHeadingItem ) { $name = 'h-'; - $mainComment = $threadItem->getThreadSummary()['oldestReply']; + $mainComment = $threadItem->getOldestReply(); } elseif ( $threadItem instanceof ContentCommentItem ) { $name = 'c-'; $mainComment = $threadItem; diff --git a/includes/ThreadItem/ContentThreadItem.php b/includes/ThreadItem/ContentThreadItem.php index d581d6b26..9b444bc85 100644 --- a/includes/ThreadItem/ContentThreadItem.php +++ b/includes/ThreadItem/ContentThreadItem.php @@ -30,6 +30,11 @@ abstract class ContentThreadItem implements JsonSerializable, ThreadItem { protected $id = null; protected $replies = []; + protected $authors = null; + protected $commentCount; + protected $oldestReply; + protected $latestReply; + /** * @param string $type `heading` or `comment` * @param int $level Indentation level @@ -46,10 +51,11 @@ abstract class ContentThreadItem implements JsonSerializable, ThreadItem { /** * Get summary metadata for a thread. - * - * @return array Information about the comments below */ - public function getThreadSummary(): array { + private function calculateThreadSummary(): void { + if ( $this->authors !== null ) { + return; + } $authors = []; $commentCount = 0; $oldestReply = null; @@ -84,43 +90,57 @@ abstract class ContentThreadItem implements JsonSerializable, ThreadItem { array_walk( $replies, $threadScan ); ksort( $authors ); - return [ - 'authors' => array_keys( $authors ), - 'commentCount' => $commentCount, - 'oldestReply' => $oldestReply, - 'latestReply' => $latestReply, - ]; + + $this->authors = array_keys( $authors ); + $this->commentCount = $commentCount; + $this->oldestReply = $oldestReply; + $this->latestReply = $latestReply; } /** - * Get the list of authors in the comment tree below this thread item. + * Get the list of authors in the tree below this thread item. * * Usually called on a HeadingItem to find all authors in a thread. * * @return string[] Author usernames */ public function getAuthorsBelow(): array { - $authors = []; - $getAuthorSet = static function ( ContentThreadItem $threadItem ) use ( &$authors, &$getAuthorSet ) { - if ( $threadItem instanceof ContentCommentItem ) { - $authors[ $threadItem->getAuthor() ] = true; - } - // Get the set of authors in the same format from each reply - foreach ( $threadItem->getReplies() as $reply ) { - $getAuthorSet( $reply ); - } - }; - - foreach ( $this->getReplies() as $reply ) { - $getAuthorSet( $reply ); - } - - ksort( $authors ); - return array_keys( $authors ); + $this->calculateThreadSummary(); + return $this->authors; } /** - * Get the list of thread items in the comment tree below this thread item. + * Get the number of comment items in the tree below this thread item. + * + * @return int + */ + public function getCommentCount(): int { + $this->calculateThreadSummary(); + return $this->commentCount; + } + + /** + * Get the latest reply in the tree below this thread item, null if there are no replies + * + * @return ContentCommentItem|null + */ + public function getLatestReply(): ?ContentCommentItem { + $this->calculateThreadSummary(); + return $this->latestReply; + } + + /** + * Get the oldest reply in the tree below this thread item, null if there are no replies + * + * @return ContentCommentItem|null + */ + public function getOldestReply(): ?ContentCommentItem { + $this->calculateThreadSummary(); + return $this->oldestReply; + } + + /** + * Get a flat list of thread items in the comment tree below this thread item. * * @return ContentThreadItem[] Thread items */ diff --git a/modules/Parser.js b/modules/Parser.js index 3c4d89c42..ba1690e57 100644 --- a/modules/Parser.js +++ b/modules/Parser.js @@ -956,7 +956,7 @@ Parser.prototype.computeId = function ( threadItem, previousItems ) { // (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 // heading ID. - var oldestComment = threadItem.getThreadSummary().oldestReply; + var oldestComment = threadItem.getOldestReply(); if ( oldestComment ) { id += '-' + oldestComment.getTimestampString(); } @@ -990,7 +990,7 @@ Parser.prototype.computeName = function ( threadItem ) { if ( threadItem instanceof HeadingItem ) { name = 'h-'; - mainComment = threadItem.getThreadSummary().oldestReply; + mainComment = threadItem.getOldestReply(); } else if ( threadItem instanceof CommentItem ) { name = 'c-'; mainComment = threadItem; diff --git a/modules/ThreadItem.js b/modules/ThreadItem.js index 68f89db45..8b7814481 100644 --- a/modules/ThreadItem.js +++ b/modules/ThreadItem.js @@ -39,6 +39,11 @@ function ThreadItem( type, level, range ) { this.warnings = []; this.rootNode = null; + + this.authors = null; + this.commentCount = null; + this.oldestReply = null; + this.latestReply = null; } OO.initClass( ThreadItem ); @@ -112,11 +117,12 @@ ThreadItem.static.newFromJSON = function ( json, rootNode ) { }; /** - * Get summary metadata for a thread. - * - * @return {Object} Information about the comments below + * Calculate summary metadata for a thread. */ -ThreadItem.prototype.getThreadSummary = function () { +ThreadItem.prototype.calculateThreadSummary = function () { + if ( this.authors ) { + return; + } var authors = {}; var commentCount = 0; var oldestReply = null; @@ -142,12 +148,10 @@ ThreadItem.prototype.getThreadSummary = function () { } this.replies.forEach( threadScan ); - return { - authors: Object.keys( authors ).sort(), - commentCount: commentCount, - oldestReply: oldestReply, - latestReply: latestReply - }; + this.authors = Object.keys( authors ).sort(); + this.commentCount = commentCount; + this.oldestReply = oldestReply; + this.latestReply = latestReply; }; /** @@ -158,18 +162,38 @@ ThreadItem.prototype.getThreadSummary = function () { * @return {string[]} Author usernames */ ThreadItem.prototype.getAuthorsBelow = function () { - var authors = {}; - function getAuthorSet( comment ) { - if ( comment.type === 'comment' ) { - authors[ comment.author ] = true; - } - // Get the set of authors in the same format from each reply - comment.replies.forEach( getAuthorSet ); - } + this.calculateThreadSummary(); + return this.authors; +}; - this.replies.forEach( getAuthorSet ); +/** + * Get the number of comment items in the tree below this thread item. + * + * @return {number} + */ +ThreadItem.prototype.getCommentCount = function () { + this.calculateThreadSummary(); + return this.commentCount; +}; - return Object.keys( authors ).sort(); +/** + * Get the latest reply in the tree below this thread item, null if there are no replies + * + * @return {CommentItem|null} + */ +ThreadItem.prototype.getLatestReply = function () { + this.calculateThreadSummary(); + return this.latestReply; +}; + +/** + * Get the oldest reply in the tree below this thread item, null if there are no replies + * + * @return {CommentItem|null} + */ +ThreadItem.prototype.getOldestReply = function () { + this.calculateThreadSummary(); + return this.oldestReply; }; /** diff --git a/tests/qunit/testUtils.js b/tests/qunit/testUtils.js index 79d7ef949..07d89e04c 100644 --- a/tests/qunit/testUtils.js +++ b/tests/qunit/testUtils.js @@ -91,6 +91,12 @@ module.exports.serializeComments = function ( parent, root ) { // Unimportant delete parent.rootNode; + // Ignore generated properties + delete parent.authors; + delete parent.commentCount; + delete parent.oldestReply; + delete parent.latestReply; + parent.replies.forEach( function ( comment ) { module.exports.serializeComments( comment, root ); } );