Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 24 |
|
0.00% |
0 / 11 |
CRAP | |
0.00% |
0 / 1 |
ContentHeadingItem | |
0.00% |
0 / 24 |
|
0.00% |
0 / 11 |
272 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getLinkableTitle | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
getLinkableId | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
getHeadlineNode | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
isUneditableSection | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setUneditableSection | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getHeadingLevel | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setHeadingLevel | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isPlaceholderHeading | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setPlaceholderHeading | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
jsonSerialize | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\DiscussionTools\ThreadItem; |
4 | |
5 | use MediaWiki\Extension\DiscussionTools\ImmutableRange; |
6 | use Wikimedia\Assert\Assert; |
7 | use Wikimedia\Parsoid\DOM\Element; |
8 | |
9 | class ContentHeadingItem extends ContentThreadItem implements HeadingItem { |
10 | use HeadingItemTrait { |
11 | jsonSerialize as traitJsonSerialize; |
12 | } |
13 | |
14 | private bool $placeholderHeading; |
15 | private int $headingLevel; |
16 | private bool $uneditableSection = false; |
17 | |
18 | // Placeholder headings must have a level higher than real headings (1-6) |
19 | private const PLACEHOLDER_HEADING_LEVEL = 99; |
20 | |
21 | /** |
22 | * @param ImmutableRange $range |
23 | * @param bool|string $transcludedFrom |
24 | * @param ?int $headingLevel Heading level (1-6). Use null for a placeholder heading. |
25 | */ |
26 | public function __construct( |
27 | ImmutableRange $range, $transcludedFrom, ?int $headingLevel |
28 | ) { |
29 | parent::__construct( 'heading', 0, $range, $transcludedFrom ); |
30 | $this->placeholderHeading = $headingLevel === null; |
31 | $this->headingLevel = $this->placeholderHeading ? static::PLACEHOLDER_HEADING_LEVEL : $headingLevel; |
32 | } |
33 | |
34 | /** |
35 | * Get a title based on the hash ID, such that it can be linked to |
36 | * |
37 | * @return string Title |
38 | */ |
39 | public function getLinkableTitle(): string { |
40 | $title = ''; |
41 | // If this comment is in 0th section, there's no section title for the edit summary |
42 | if ( !$this->isPlaceholderHeading() ) { |
43 | $id = $this->getLinkableId(); |
44 | if ( $id ) { |
45 | // Replace underscores with spaces to undo Sanitizer::escapeIdInternal(). |
46 | // This assumes that $wgFragmentMode is [ 'html5', 'legacy' ] or [ 'html5' ], |
47 | // otherwise the escaped IDs are super garbled and can't be unescaped reliably. |
48 | $title = str_replace( '_', ' ', $id ); |
49 | } |
50 | // else: Not a real section, probably just HTML markup in wikitext |
51 | } |
52 | return $title; |
53 | } |
54 | |
55 | /** |
56 | * Return the ID found on the headline node, if it has one. |
57 | * |
58 | * In Parsoid HTML, it is stored in the `<hN id>` attribute. |
59 | * In legacy parser HTML, it is stored in the `<hN data-mw-anchor>` attribute. |
60 | * In integration tests and in JS, things are a little bit wilder than that. |
61 | * |
62 | * @return string |
63 | */ |
64 | public function getLinkableId(): string { |
65 | $headline = $this->getHeadlineNode(); |
66 | return ( $headline->getAttribute( 'id' ) ?: $headline->getAttribute( 'data-mw-anchor' ) ) ?? ''; |
67 | } |
68 | |
69 | /** |
70 | * Return the node on which the ID attribute is set. |
71 | * |
72 | * @return Element Headline node, normally a `<h1>`-`<h6>` element (unless it's a placeholder heading). |
73 | * In integration tests and in JS, it can be a `<span class="mw-headline">` (see T363031). |
74 | */ |
75 | public function getHeadlineNode(): Element { |
76 | // This value comes from CommentUtils::getHeadlineNode(), this function just guarantees the type |
77 | $headline = $this->getRange()->startContainer; |
78 | Assert::precondition( $headline instanceof Element, 'HeadingItem refers to an element node' ); |
79 | return $headline; |
80 | } |
81 | |
82 | public function isUneditableSection(): bool { |
83 | return $this->uneditableSection; |
84 | } |
85 | |
86 | /** |
87 | * @param bool $uneditableSection The heading represents a section that can't be |
88 | * edited on its own. |
89 | */ |
90 | public function setUneditableSection( bool $uneditableSection ): void { |
91 | $this->uneditableSection = $uneditableSection; |
92 | } |
93 | |
94 | /** |
95 | * @return int Heading level (1-6) |
96 | */ |
97 | public function getHeadingLevel(): int { |
98 | return $this->headingLevel; |
99 | } |
100 | |
101 | /** |
102 | * @param int $headingLevel Heading level (1-6) |
103 | */ |
104 | public function setHeadingLevel( int $headingLevel ): void { |
105 | $this->headingLevel = $headingLevel; |
106 | } |
107 | |
108 | public function isPlaceholderHeading(): bool { |
109 | return $this->placeholderHeading; |
110 | } |
111 | |
112 | public function setPlaceholderHeading( bool $placeholderHeading ): void { |
113 | $this->placeholderHeading = $placeholderHeading; |
114 | } |
115 | |
116 | /** |
117 | * @inheritDoc |
118 | */ |
119 | public function jsonSerialize( bool $deep = false, ?callable $callback = null ): array { |
120 | $data = $this->traitJsonSerialize( $deep, $callback ); |
121 | |
122 | // When this is false (which is most of the time), omit the key for efficiency |
123 | if ( $this->isUneditableSection() ) { |
124 | $data[ 'uneditableSection' ] = true; |
125 | } |
126 | return $data; |
127 | } |
128 | } |