Improve ThreadItem documentation

Change-Id: Ia266fc22b02af0edbb32f356b4e0d92fe3a4da5f
This commit is contained in:
Ed Sanders 2020-06-25 13:23:17 +01:00 committed by Bartosz Dziewoński
parent c7b7fe5d66
commit d5376e28fc
12 changed files with 170 additions and 126 deletions

View file

@ -9,8 +9,8 @@
"scripts": {
"test": [
"parallel-lint . --exclude vendor --exclude node_modules",
"minus-x check .",
"phpcs -p -s"
"phpcs -p -s",
"minus-x check ."
],
"fix": [
"minus-x fix .",

View file

@ -13,9 +13,12 @@ class CommentItem extends ThreadItem {
/**
* @param int $level
* @param ImmutableRange $range
* @param ImmutableRange[] $signatureRanges
* @param string|null $timestamp
* @param string|null $author
* @param ImmutableRange[] $signatureRanges Objects describing the extent of signatures (plus
* timestamps) for this comment. There is always at least one signature, but there may be
* multiple. The author and timestamp of the comment is determined from the first signature.
* The last node in every signature range is a node containing the timestamp.
* @param string|null $timestamp Timestamp
* @param string|null $author Comment author's username
*/
public function __construct(
int $level, ImmutableRange $range,

View file

@ -693,31 +693,17 @@ class CommentParser {
* This function would return a structure like:
*
* [
* [ 'type' => 'heading', 'level' => 0, 'range' => (h2: A) },
* [ 'type' => 'comment', 'level' => 1, 'range' => (p: B) },
* [ 'type' => 'comment', 'level' => 2, 'range' => (li: C, li: C) },
* [ 'type' => 'comment', 'level' => 3, 'range' => (li: D) },
* [ 'type' => 'comment', 'level' => 4, 'range' => (li: E) },
* [ 'type' => 'comment', 'level' => 4, 'range' => (li: F) },
* [ 'type' => 'comment', 'level' => 2, 'range' => (li: G) },
* [ 'type' => 'comment', 'level' => 1, 'range' => (p: H) },
* [ 'type' => 'comment', 'level' => 2, 'range' => (li: I) }
* 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) } )
* ]
*
* The elements of the array are ThreadItem objects with the following fields:
* - 'type' (string): 'heading' or 'comment'
* - 'range' (ImmutableRange): The extent of the comment, including the signature and timestamp.
* Comments can start or end in the middle of a DOM node.
* - 'signatureRanges' (ImmutableRange): The extents of the comment's signatures (plus timestamps).
* There is always at least one signature, but there may be multiple.
* The author and timestamp of the comment is determined from the
* first signature. The last node in every signature range is
* a node containing the timestamp.
* - 'level' (int): Indentation level of the comment. Headings are 0, comments start at 1.
* - 'timestamp' (string): ISO 8601 timestamp in UTC (ending in 'Z'). Not set for headings.
* - 'author' (string|null): Comment author's username, null for unsigned comments.
* Not set for headings.
*
* @param DOMElement $rootNode
* @return ThreadItem[] Thread items
*/
@ -873,29 +859,24 @@ class CommentParser {
* This function would return a structure like:
*
* [
* [ 'type' => 'heading', 'level' => 0, 'range' => (h2: A), 'replies' => [
* [ 'type' => 'comment', 'level' => 1, 'range' => (p: B), 'replies' => [
* [ 'type' => 'comment', 'level' => 2, 'range' => (li: C, li: C), 'replies' => [
* [ 'type' => 'comment', 'level' => 3, 'range' => (li: D), 'replies' => [
* [ 'type' => 'comment', 'level' => 4, 'range' => (li: E), 'replies' => [] ],
* [ 'type' => 'comment', 'level' => 4, 'range' => (li: F), 'replies': [] ],
* ] ],
* ] ],
* [ 'type' => 'comment', 'level' => 2, 'range' => (li: G), 'replies' => [] ],
* ] ],
* [ 'type' => 'comment', 'level' => 1, 'range' => (p: H), 'replies' => [
* [ 'type' => 'comment', 'level' => 2, 'range' => (li: I), 'replies' => [] ],
* ] ],
* ] ],
* 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: [] },
* ] },
* ] } )
* ]
*
* @param ThreadItem[] &$comments Result of #getComments, will be modified to add more properties
* @return HeadingItem[] Tree structure of comments, using the same objects as `comments`.
* Top-level items are the headings. The following properties are added:
* - id: Unique ID (within the page) for this comment, intended to be used to
* find this comment in other revisions of the same page
* - replies: Comment objects which are replies to this comment
* - parent: Comment object which this is a reply to (null for headings)
* @return HeadingItem[] Tree structure of comments, top-level items are the headings.
*/
public function groupThreads( array &$comments ) : array {
$threads = [];

View file

@ -7,7 +7,7 @@ class HeadingItem extends ThreadItem {
/**
* @param ImmutableRange $range
* @param bool $placeholderHeading
* @param bool $placeholderHeading Item doesn't correspond to a real heading (e.g. 0th section)
*/
public function __construct(
ImmutableRange $range, bool $placeholderHeading = false

View file

@ -2,6 +2,9 @@
namespace MediaWiki\Extension\DiscussionTools;
/**
* A thread item, either a heading or a comment
*/
abstract class ThreadItem {
private $type;
private $range;
@ -11,9 +14,10 @@ abstract class ThreadItem {
private $replies = [];
/**
* @param string $type
* @param int $level
* @param ImmutableRange $range
* @param string $type `heading` or `comment`
* @param int $level Item level in the thread tree
* @param ImmutableRange $range Object describing the extent of the comment, including the
* signature and timestamp.
*/
public function __construct(
string $type, int $level, ImmutableRange $range
@ -52,7 +56,7 @@ abstract class ThreadItem {
}
/**
* @return CommentItem[] Thread item replies
* @return CommentItem[] Replies to this thread item
*/
public function getReplies() : array {
return $this->replies;

View file

@ -1,5 +1,24 @@
var ThreadItem = require( './ThreadItem.js' );
/**
* @external moment
*/
/**
* A comment item
*
* @class CommentItem
* @extends {ThreadItem}
* @constructor
* @param {number} level
* @param {Object} range
* @param {Object[]} [signatureRanges] Objects describing the extent of signatures (plus
* timestamps) for this comment. There is always at least one signature, but there may be
* multiple. The author and timestamp of the comment is determined from the first signature.
* The last node in every signature range is a node containing the timestamp.
* @param {moment} [timestamp] Timestamp (Moment object)
* @param {string} [author] Comment author's username
*/
function CommentItem( level, range, signatureRanges, timestamp, author ) {
// Parent constructor
CommentItem.super.call( this, 'comment', level, range );
@ -7,6 +26,17 @@ function CommentItem( level, range, signatureRanges, timestamp, author ) {
this.signatureRanges = signatureRanges || [];
this.timestamp = timestamp || null;
this.author = author || null;
/**
* @member {string[]} Comment warnings
*/
// TODO: Should probably initialise, but our tests assert it is unset
// this.warnings = [];
/**
* @member {ThreadItem} Parent thread item
*/
this.parent = null;
}
OO.inheritClass( CommentItem, ThreadItem );

View file

@ -1,9 +1,19 @@
var ThreadItem = require( './ThreadItem.js' );
/**
* A heading item
*
* @class HeadingItem
* @extends {ThreadItem}
* @constructor
* @param {Object} range
* @param {boolean} [placeholderHeading] Item doesn't correspond to a real heading (e.g. 0th section)
*/
function HeadingItem( range, placeholderHeading ) {
// Parent constructor
HeadingItem.super.call( this, 'heading', 0, range );
// TODO: Should probably always initialise, but our tests assert it is unset
if ( placeholderHeading ) {
this.placeholderHeading = true;
}

View file

@ -1,9 +1,32 @@
/**
* @external CommentItem
*/
/**
* A thread item, either a heading or a comment
*
* @class ThreadItem
* @constructor
* @param {string} type `heading` or `comment`
* @param {number} level Item level in the thread tree
* @param {Object} range Object describing the extent of the comment, including the
* signature and timestamp. It has the same properties as a Range object: `startContainer`,
* `startOffset`, `endContainer`, `endOffset` (we don't use a real Range because they change
* magically when the DOM structure changes).
*/
function ThreadItem( type, level, range ) {
this.type = type;
this.level = level;
this.range = range;
/**
* @member {string} Unique ID (within the page) for this comment, intended to be used to
* find this comment in other revisions of the same page
*/
this.id = null;
/**
* @member {CommentItem[]} Replies to this thread item
*/
this.replies = [];
}

View file

@ -178,9 +178,7 @@ function getTimestampRegexp( format, digitsRegexp, tzAbbrs ) {
* @param {string} localTimezone Local timezone IANA name, e.g. `America/New_York`
* @param {Object} tzAbbrs Map of localised timezone abbreviations to IANA abbreviations
* for the local timezone, e.g. `{EDT: "EDT", EST: "EST"}`
* @return {Function} Parser function
* @return {Array} return.match Regexp match data
* @return {Object} return.return Moment object
* @return {TimestampParser} Timestamp parser function
*/
function getTimestampParser( format, digits, localTimezone, tzAbbrs ) {
var p, code, endQuote, matchingGroups = [];
@ -243,6 +241,16 @@ function getTimestampParser( format, digits, localTimezone, tzAbbrs ) {
);
}
/**
* @typedef {function(Array):moment} TimestampParser
*/
/**
* Timestamp parser
*
* @param {Array} match RegExp match data
* @return {moment} Moment date object
*/
return function timestampParser( match ) {
var
year = 0,
@ -363,9 +371,7 @@ function getLocalTimestampRegexp() {
* This calls #getTimestampParser with predefined data for the current wiki.
*
* @private
* @return {Function} Parser function
* @return {Array} return.match Regexp match data
* @return {Date} return.return
* @return {TimestampParser} Timestamp parser function
*/
function getLocalTimestampParser() {
return getTimestampParser(
@ -395,10 +401,10 @@ function acceptOnlyNodesAllowingComments( node ) {
* Find all timestamps within a DOM subtree.
*
* @param {HTMLElement} rootNode Node to search
* @return {Array[]} Results. Each result is a two-element array.
* @return {Text} return.0 Text node containing the timestamp
* @return {Array} return.1 Regexp match data, which specifies the location of the match, and which
* can be parsed using #getLocalTimestampParser
* @return {[Text, Array]} Results. Each result is a tuple containing:
* - Text node containing the timestamp
* - Regexp match data, which specifies the location of the match, and which
* can be parsed using #getLocalTimestampParser
*/
function findTimestamps( rootNode ) {
var
@ -496,10 +502,10 @@ function getTitleFromUrl( url ) {
* @private
* @param {Text} timestampNode Text node
* @param {Node} [until] Node to stop searching at
* @return {Array} Result, a two-element array
* @return {Node[]} return.0 Sibling nodes comprising the signature, in reverse order (with
* @return {[Node[], string|null]} Result, a tuple contaning:
* - Sibling nodes comprising the signature, in reverse order (with
* `timestampNode` or its parent node as the first element)
* @return {string|null} return.1 Username, null for unsigned comments
* - Username, null for unsigned comments
*/
function findSignature( timestampNode, until ) {
var node, sigNodes, sigUsername, length, lastLinkNode, links, nodes;
@ -680,32 +686,19 @@ function nextInterestingLeafNode( node, rootNode ) {
* This function would return a structure like:
*
* [
* { type: 'heading', level: 0, range: (h2: A) },
* { type: 'comment', level: 1, range: (p: B) },
* { type: 'comment', level: 2, range: (li: C, li: C) },
* { type: 'comment', level: 3, range: (li: D) },
* { type: 'comment', level: 4, range: (li: E) },
* { type: 'comment', level: 4, range: (li: F) },
* { type: 'comment', level: 2, range: (li: G) },
* { type: 'comment', level: 1, range: (p: H) },
* { type: 'comment', level: 2, range: (li: I) }
* 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) } )
* ]
*
* @param {HTMLElement} rootNode
* @return {ThreadItem[]} Results. Each result is an object.
* @return {string} return.type `heading` or `comment`
* @return {Object} return.range Object describing the extent of the comment, including the
* signature and timestamp. It has the same properties as a Range object: `startContainer`,
* `startOffset`, `endContainer`, `endOffset` (we don't use a real Range because they change
* magically when the DOM structure changes).
* @return {Object[]} [return.signatureRanges] Objects describing the extent of signatures (plus
* timestamps) for this comment. There is always at least one signature, but there may be
* multiple. The author and timestamp of the comment is determined from the first signature.
* The last node in every signature range is a node containing the timestamp.
* @return {number} return.level Indentation level of the comment. Headings are `0`, comments start
* at `1`.
* @return {Object} [return.timestamp] Timestamp (Moment object), undefined for headings
* @return {string} [return.author] Comment author's username, undefined for headings
* @return {ThreadItem[]} Thread items
*/
function getComments( rootNode ) {
var
@ -859,29 +852,24 @@ function getComments( rootNode ) {
* This function would return a structure like:
*
* [
* { type: 'heading', level: 0, range: (h2: A), replies: [
* { type: 'comment', level: 1, range: (p: B), replies: [
* { type: 'comment', level: 2, range: (li: C, li: C), replies: [
* { type: 'comment', level: 3, range: (li: D), replies: [
* { type: 'comment', level: 4, range: (li: E), replies: [] },
* { type: 'comment', level: 4, range: (li: F), replies: [] },
* 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: [] },
* ] },
* ] },
* { type: 'comment', level: 2, range: (li: G), replies: [] },
* CommentItem( { level: 2, range: (li: G), replies: [] },
* ] },
* { type: 'comment', level: 1, range: (p: H), replies: [
* { type: 'comment', level: 2, range: (li: I), replies: [] },
* CommentItem( { level: 1, range: (p: H), replies: [
* CommentItem( { level: 2, range: (li: I), replies: [] },
* ] },
* ] },
* ] } )
* ]
*
* @param {ThreadItem} comments Result of #getComments
* @return {HeadingItem[]} Tree structure of comments, using the same objects as `comments`. Top-level
* items are the headings. The following properties are added:
* @return {string} return.id Unique ID (within the page) for this comment, intended to be used to
* find this comment in other revisions of the same page
* @return {Object[]} return.replies Comment objects which are replies to this comment
* @return {Object|null} return.parent Comment object which this is a reply to (null for headings)
* @param {ThreadItem[]} comments Result of #getComments
* @return {HeadingItem[]} Tree structure of comments, top-level items are the headings.
*/
function groupThreads( comments ) {
var

41
package-lock.json generated
View file

@ -1017,16 +1017,16 @@
}
},
"eslint-config-wikimedia": {
"version": "0.16.1",
"resolved": "https://registry.npmjs.org/eslint-config-wikimedia/-/eslint-config-wikimedia-0.16.1.tgz",
"integrity": "sha512-VFP+zOaehZgbcH1TCeH6iBZuYv83mZMvu+YYntblbmFrw36Oo9lcNWiUL95SAE+5JtkGtAy51NLE1T61XJYn5w==",
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/eslint-config-wikimedia/-/eslint-config-wikimedia-0.16.2.tgz",
"integrity": "sha512-tQikCZT2k3z9UzvRDFAUOpVSwE/MEmKIUQQraFh9tgyPOmRY6fVkMONcFqdEuz8eyg2syW9MNvT2d1SGSMLfBg==",
"dev": true,
"requires": {
"eslint": "^7.1.0",
"eslint": "^7.2.0",
"eslint-plugin-es": "^3.0.1",
"eslint-plugin-jsdoc": "^26.0.0",
"eslint-plugin-jsdoc": "^27.1.2",
"eslint-plugin-json": "^2.1.1",
"eslint-plugin-mediawiki": "^0.2.4",
"eslint-plugin-mediawiki": "^0.2.5",
"eslint-plugin-mocha": "^7.0.1",
"eslint-plugin-no-jquery": "^2.4.1",
"eslint-plugin-node": "^11.1.0",
@ -1046,12 +1046,12 @@
}
},
"eslint-plugin-jsdoc": {
"version": "26.0.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-26.0.2.tgz",
"integrity": "sha512-KtZjqtM3Z8x84vQBFKGUyBbZRGXYHVWSJ2XyYSUTc8KhfFrvzQ/GXPp6f1M1/YCNzP3ImD5RuDNcr+OVvIZcBA==",
"version": "27.1.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-27.1.2.tgz",
"integrity": "sha512-iWrG2ZK4xrxamoMkoyzgkukdmfqWc5Ncd6K+CnwRgxrbwjQQpzmt5Kl8GB0l12R0oUK2AF+9tGFJKNGzuyz79Q==",
"dev": true,
"requires": {
"comment-parser": "^0.7.4",
"comment-parser": "^0.7.5",
"debug": "^4.1.1",
"jsdoctypeparser": "^6.1.0",
"lodash": "^4.17.15",
@ -1089,12 +1089,13 @@
}
},
"eslint-plugin-mediawiki": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/eslint-plugin-mediawiki/-/eslint-plugin-mediawiki-0.2.4.tgz",
"integrity": "sha512-tvvLPTwXp5YpCh3tbfSq/tOFRRcgrje1GVOz+91qBzuHOY6gHOesXQSrryeG33rbbRztktIa7IggpWmi2Rsu3A==",
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/eslint-plugin-mediawiki/-/eslint-plugin-mediawiki-0.2.5.tgz",
"integrity": "sha512-Xs5G4f1EnS6+9gFWkk28nWA9xcOEPx7YZEGsMYGLelZRAF+2DmV/PigF5N5VqoOkNBpwcbXqLD8wLfkg29aF8w==",
"dev": true,
"requires": {
"eslint-plugin-vue": "^6.2.2"
"eslint-plugin-vue": "^6.2.2",
"upath": "^1.2.0"
}
},
"eslint-plugin-mocha": {
@ -1108,9 +1109,9 @@
}
},
"eslint-plugin-no-jquery": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-no-jquery/-/eslint-plugin-no-jquery-2.4.1.tgz",
"integrity": "sha512-pHyBFyDgUj/cQD1QNh9SiLaicFJyqeFGDEGRLUn3HezETvsCSi1oeRqjotkL6xAyvwyTiih5d3dhfon0DUvZJA==",
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-no-jquery/-/eslint-plugin-no-jquery-2.5.0.tgz",
"integrity": "sha512-RrQ380mUJJKdjgpQ/tZAJ3B3W1n3LbVmULooS2Pv5pUDcc5uVHVSJMTdUlsbvQyfo6hWP2LJ4FbOoDzENWcF7A==",
"dev": true
},
"eslint-plugin-node": {
@ -3760,6 +3761,12 @@
"unist-util-is": "^4.0.0"
}
},
"upath": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz",
"integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==",
"dev": true
},
"uri-js": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",

View file

@ -6,7 +6,7 @@
"test": "grunt test"
},
"devDependencies": {
"eslint-config-wikimedia": "0.16.1",
"eslint-config-wikimedia": "0.16.2",
"grunt": "1.1.0",
"grunt-banana-checker": "0.9.0",
"grunt-eslint": "23.0.0",

View file

@ -1,7 +1,5 @@
{
"root": true,
"extends": [
"../../.eslintrc",
"wikimedia/qunit"
],
"rules": {