Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.86% covered (success)
94.86%
277 / 292
81.48% covered (warning)
81.48%
22 / 27
CRAP
0.00% covered (danger)
0.00%
0 / 1
CommentUtils
94.86% covered (success)
94.86%
277 / 292
81.48% covered (warning)
81.48%
22 / 27
163.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%
20 / 20
100.00% covered (success)
100.00%
1 / 1
15
 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
5.31
 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%
22 / 22
100.00% covered (success)
100.00%
1 / 1
6
 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 LogicException;
6use MediaWiki\Config\Config;
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 const BLOCK_ELEMENT_TYPES = [
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::BLOCK_ELEMENT_TYPES, 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    private const NO_ELEMENT_CHILDREN_ELEMENT_TYPES = [
98        // https://html.spec.whatwg.org/multipage/syntax.html#elements-2
99        // Void elements
100        'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
101        'link', 'meta', 'param', 'source', 'track', 'wbr',
102        // Raw text elements
103        'script', 'style',
104        // Escapable raw text elements
105        'textarea', 'title',
106        // Foreign elements
107        'math', 'svg',
108        // Treated like text when scripting is enabled in the parser
109        // https://html.spec.whatwg.org/#the-noscript-element
110        'noscript',
111        // Replaced elements (that aren't already included above)
112        // https://html.spec.whatwg.org/multipage/rendering.html#replaced-elements
113        // They might allow element children, but they aren't rendered on the page.
114        'audio', 'canvas', 'iframe', 'object', 'video',
115    ];
116
117    /**
118     * @param Node $node
119     * @return bool If true, node can't have element children. If false, it's complicated.
120     */
121    public static function cantHaveElementChildren( Node $node ): bool {
122        return (
123            $node instanceof Comment ||
124            ( $node instanceof Element && (
125                in_array( strtolower( $node->tagName ), static::NO_ELEMENT_CHILDREN_ELEMENT_TYPES, true ) ||
126                // Thumbnail wrappers generated by MediaTransformOutput::linkWrap (T301427),
127                // for compatibility with TimedMediaHandler.
128                // There is no better way to detect them, and we can't insert markers here,
129                // because the media DOM CSS depends on specific tag names and their order :(
130                // TODO See if we can remove this condition when wgParserEnableLegacyMediaDOM=false