Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
94.76% |
271 / 286 |
|
81.48% |
22 / 27 |
CRAP | |
0.00% |
0 / 1 |
CommentUtils | |
94.76% |
271 / 286 |
|
81.48% |
22 / 27 |
167.88 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isBlockElement | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
isRenderingTransparentNode | |
100.00% |
22 / 22 |
|
100.00% |
1 / 1 |
11 | |||
isOurGeneratedNode | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
cantHaveElementChildren | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
7 | |||
isCommentSeparator | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
15 | |||
isCommentContent | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
childIndexOf | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
contains | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
closestElement | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
closestElementWithSibling | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
72 | |||
getTranscludedFromElement | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
10 | |||
getHeadlineNode | |
66.67% |
6 / 9 |
|
0.00% |
0 / 1 |
5.93 | |||
htmlTrim | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getIndentLevel | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
6 | |||
getCoveredSiblings | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
5 | |||
getFullyCoveredSiblings | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
5 | |||
unwrapParsoidSections | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
getTitleFromUrl | |
100.00% |
22 / 22 |
|
100.00% |
1 / 1 |
6 | |||
linearWalk | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
5 | |||
linearWalkBackwards | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
5 | |||
getRangeFirstNode | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
getRangeLastNode | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
compareRanges | |
88.00% |
22 / 25 |
|
0.00% |
0 / 1 |
23.91 | |||
compareRangesAlmostEqualBoundaries | |
100.00% |
28 / 28 |
|
100.00% |
1 / 1 |
15 | |||
isSingleCommentSignedBy | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
9 | |||
getNewTopicsSubscriptionId | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\DiscussionTools; |
4 | |
5 | use LogicException; |
6 | use MediaWiki\Config\Config; |
7 | use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentCommentItem; |
8 | use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentThreadItem; |
9 | use MediaWiki\MainConfigNames; |
10 | use MediaWiki\Title\Title; |
11 | use Wikimedia\Assert\Assert; |
12 | use Wikimedia\Parsoid\DOM\Comment; |
13 | use Wikimedia\Parsoid\DOM\Element; |
14 | use Wikimedia\Parsoid\DOM\Node; |
15 | use Wikimedia\Parsoid\DOM\Text; |
16 | use Wikimedia\Parsoid\Utils\DOMCompat; |
17 | |
18 | class 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 :( |