diff --git a/composer.json b/composer.json index b519f1156..5fc762355 100644 --- a/composer.json +++ b/composer.json @@ -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 .", diff --git a/includes/CommentItem.php b/includes/CommentItem.php index 75203a21c..1bce94e06 100644 --- a/includes/CommentItem.php +++ b/includes/CommentItem.php @@ -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, diff --git a/includes/CommentParser.php b/includes/CommentParser.php index 679438536..a04d68654 100644 --- a/includes/CommentParser.php +++ b/includes/CommentParser.php @@ -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 = []; diff --git a/includes/HeadingItem.php b/includes/HeadingItem.php index c1ffe210f..a6e5c0e50 100644 --- a/includes/HeadingItem.php +++ b/includes/HeadingItem.php @@ -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 diff --git a/includes/ThreadItem.php b/includes/ThreadItem.php index 87040682a..6d929e8d0 100644 --- a/includes/ThreadItem.php +++ b/includes/ThreadItem.php @@ -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; diff --git a/modules/CommentItem.js b/modules/CommentItem.js index b89b23720..5ac4550bc 100644 --- a/modules/CommentItem.js +++ b/modules/CommentItem.js @@ -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 ); diff --git a/modules/HeadingItem.js b/modules/HeadingItem.js index e8a479c70..63eaa1b92 100644 --- a/modules/HeadingItem.js +++ b/modules/HeadingItem.js @@ -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; } diff --git a/modules/ThreadItem.js b/modules/ThreadItem.js index 3af2bd74d..62d372a49 100644 --- a/modules/ThreadItem.js +++ b/modules/ThreadItem.js @@ -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 = []; } diff --git a/modules/parser.js b/modules/parser.js index cbf84ae08..8a52ddd2f 100644 --- a/modules/parser.js +++ b/modules/parser.js @@ -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 diff --git a/package-lock.json b/package-lock.json index edb79874c..d350d6622 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 6cc013a7e..a252d7fa6 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/tests/qunit/.eslintrc.json b/tests/qunit/.eslintrc.json index d3141c9df..183e54a1d 100644 --- a/tests/qunit/.eslintrc.json +++ b/tests/qunit/.eslintrc.json @@ -1,7 +1,5 @@ { - "root": true, "extends": [ - "../../.eslintrc", "wikimedia/qunit" ], "rules": {