Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.76% covered (success)
94.76%
271 / 286
81.48% covered (warning)
81.48%
22 / 27
CRAP
0.00% covered (danger)
0.00%
0 / 1
CommentUtils
94.76% covered (success)
94.76%
271 / 286
81.48% covered (warning)
81.48%
22 / 27
158.47
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isBlockElement
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 isRenderingTransparentNode
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
11
 isOurGeneratedNode
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 cantHaveElementChildren
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
7
 isCommentSeparator
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
12
 isCommentContent
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 childIndexOf
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 contains
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 closestElement
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 closestElementWithSibling
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
72
 getTranscludedFromElement
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
10
 getHeadlineNodeAndOffset
76.92% covered (warning)
76.92%
10 / 13
0.00% covered (danger)
0.00%
0 / 1
4.20
 htmlTrim
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getIndentLevel
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
6
 getCoveredSiblings
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 getFullyCoveredSiblings
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
5
 unwrapParsoidSections
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getTitleFromUrl
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
5
 linearWalk
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 linearWalkBackwards
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 getRangeFirstNode
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getRangeLastNode
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 compareRanges
88.00% covered (warning)
88.00%
22 / 25
0.00% covered (danger)
0.00%
0 / 1
23.91
 compareRangesAlmostEqualBoundaries
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
1 / 1
15
 isSingleCommentSignedBy
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
5
 getNewTopicsSubscriptionId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\DiscussionTools;
4
5use Config;
6use LogicException;
7use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentCommentItem;
8use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentThreadItem;
9use MediaWiki\MainConfigNames;
10use MediaWiki\Title\Title;
11use Wikimedia\Assert\Assert;
12use Wikimedia\Parsoid\DOM\Comment;
13use Wikimedia\Parsoid\DOM\Element;
14use Wikimedia\Parsoid\DOM\Node;
15use Wikimedia\Parsoid\DOM\Text;
16use Wikimedia\Parsoid\Utils\DOMCompat;
17
18class CommentUtils {
19
20    private function __construct() {
21    }
22
23    private static $blockElementTypes = [
24        'div', 'p',
25        // Tables
26        'table', 'tbody', 'thead', 'tfoot', 'caption', 'th', 'tr', 'td',
27        // Lists
28        'ul', 'ol', 'li', 'dl', 'dt', 'dd',
29        // HTML5 heading content
30        'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hgroup',
31        // HTML5 sectioning content
32        'article', 'aside', 'body', 'nav', 'section', 'footer', 'header', 'figure',
33        'figcaption', 'fieldset', 'details', 'blockquote',
34        // Other
35        'hr', 'button', 'canvas', 'center', 'col', 'colgroup', 'embed',
36        'map', 'object', 'pre', 'progress', 'video'
37    ];
38
39    /**
40     * @param Node $node
41     * @return bool Node is a block element
42     */
43    public static function isBlockElement( Node $node ): bool {
44        return $node instanceof Element &&
45            in_array( strtolower( $node->tagName ), static::$blockElementTypes, true );
46    }
47
48    private const SOL_TRANSPARENT_LINK_REGEX =
49        '/(?:^|\s)mw:PageProp\/(?:Category|redirect|Language)(?=$|\s)/D';
50
51    /**
52     * @param Node $node
53     * @return bool Node is considered a rendering-transparent node in Parsoid
54     */
55    public static function isRenderingTransparentNode( Node $node ): bool {
56        $nextSibling = $node->nextSibling;
57        return (
58            $node instanceof Comment ||
59            $node instanceof Element && (
60                strtolower( $node->tagName ) === 'meta' ||
61                (
62                    strtolower( $node->tagName ) === 'link' &&
63                    preg_match( static::SOL_TRANSPARENT_LINK_REGEX, $node->getAttribute( 'rel' ) ?? '' )
64                ) ||
65                // Empty inline templates, e.g. tracking templates. (T269036)
66                // But not empty nodes that are just the start of a non-empty template about-group. (T290940)
67                (
68                    strtolower( $node->tagName ) === 'span' &&
69                    in_array( 'mw:Transclusion', explode( ' ', $node->getAttribute( 'typeof' ) ?? '' ), true ) &&
70                    !static::htmlTrim( DOMCompat::getInnerHTML( $node ) ) &&
71                    (
72                        !$nextSibling || !( $nextSibling instanceof Element ) ||
73                        // Maybe we should be checking all of the about-grouped nodes to see if they're empty,
74                        // but that's prooobably not needed in practice, and it leads to a quadratic worst case.
75                        $nextSibling->getAttribute( 'about' ) !== $node->getAttribute( 'about' )
76                    )
77                )
78            )
79        );
80    }
81
82    /**
83     * @param Node $node
84     * @return bool Node was added to the page by DiscussionTools
85     */
86    public static function isOurGeneratedNode( Node $node ): bool {
87        return $node instanceof Element && (
88            DOMCompat::getClassList( $node )->contains( 'ext-discussiontools-init-replylink-buttons' ) ||
89            $node->hasAttribute( 'data-mw-comment-start' ) ||
90            $node->hasAttribute( 'data-mw-comment-end' )
91        );
92    }
93
94    /**
95     * Elements which can't have element children (but some may have text content).
96     *
97     * @var string[]
98     */
99    private static array $noElementChildrenElementTypes = [
100        // https://html.spec.whatwg.org/multipage/syntax.html#elements-2
101        // Void elements
102        'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
103        'link', 'meta', 'param', 'source', 'track', 'wbr',
104        // Raw text elements
105        'script', 'style',
106        // Escapable raw text elements
107        'textarea', 'title',
108        // Foreign elements
109        'math', 'svg',
110        // Treated like text when scripting is enabled in the parser
111        // https://html.spec.whatwg.org/#the-noscript-element
112        'noscript',
113        // Replaced elements (that aren't already included above)
114        // https://html.spec.whatwg.org/multipage/rendering.html#replaced-elements
115        // They might allow element children, but they aren't rendered on the page.
116        'audio', 'canvas', 'iframe', 'object', 'video',
117    ];
118
119    /**
120     * @param Node $node
121     * @return bool If true, node can't have element children. If false, it's complicated.
122     */
123    public static function cantHaveElementChildren( Node $node ): bool {
124        return (
125            $node instanceof Comment ||
126            $node instanceof Element && (
127                in_array( strtolower( $node->tagName ), static::$noElementChildrenElementTypes, true ) ||
128                // Thumbnail wrappers generated by MediaTransformOutput::linkWrap (T301427),
129                // for compatibility with TimedMediaHandler.
130                // There is no better way to detect them, and we can't insert markers here,
131                // because the media DOM CSS depends on specific tag names and their order :(
132                // TODO See if we can remove this condition when wgParserEnableLegacyMediaDOM=false
133                // is enabled everywhere.
134                (
135                    in_array( strtolower( $node->tagName ), [ 'a', 'span' ], true ) &&
136                    $node->firstChild &&
137                    // We always step inside a child node so this can't be infinite, silly Phan
138                    // @phan-suppress-next-line PhanInfiniteRecursion
139                    static::cantHaveElementChildren( $node->firstChild )
140                ) ||
141                // Do not insert anything inside figures when using wgParserEnableLegacyMediaDOM=false,
142                // because their CSS can't handle it (T320285).
143                strtolower( $node->tagName ) === 'figure'