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 |
158.47 | |
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% |
18 / 18 |
|
100.00% |
1 / 1 |
12 | |||
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 | |||
getHeadlineNodeAndOffset | |
76.92% |
10 / 13 |
|
0.00% |
0 / 1 |
4.20 | |||
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% |
18 / 18 |
|
100.00% |
1 / 1 |
5 | |||
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% |
17 / 17 |
|
100.00% |
1 / 1 |
5 | |||
getNewTopicsSubscriptionId | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\DiscussionTools; |
4 | |
5 | use Config; |
6 | use LogicException; |
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 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' |