Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
22.12% |
25 / 113 |
|
20.00% |
1 / 5 |
CRAP | |
0.00% |
0 / 1 |
Ref | |
22.12% |
25 / 113 |
|
20.00% |
1 / 5 |
648.09 | |
0.00% |
0 / 1 |
sourceToDom | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
12 | |||
processAttributeEmbeddedHTML | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
lintHandler | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
domToWikitext | |
41.51% |
22 / 53 |
|
0.00% |
0 / 1 |
82.83 | |||
diffHandler | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
90 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | // phpcs:disable MediaWiki.WhiteSpace.SpaceBeforeSingleLineComment.NewLineComment |
4 | |
5 | namespace Cite\Parsoid; |
6 | |
7 | use Closure; |
8 | use Exception; |
9 | use Wikimedia\Parsoid\DOM\DocumentFragment; |
10 | use Wikimedia\Parsoid\DOM\Element; |
11 | use Wikimedia\Parsoid\Ext\DOMDataUtils; |
12 | use Wikimedia\Parsoid\Ext\DOMUtils; |
13 | use Wikimedia\Parsoid\Ext\ExtensionTagHandler; |
14 | use Wikimedia\Parsoid\Ext\ParsoidExtensionAPI; |
15 | use Wikimedia\Parsoid\Utils\DOMCompat; |
16 | |
17 | /** |
18 | * Simple token transform version of the Ref extension tag. |
19 | * @license GPL-2.0-or-later |
20 | */ |
21 | class Ref extends ExtensionTagHandler { |
22 | |
23 | /** @inheritDoc */ |
24 | public function sourceToDom( |
25 | ParsoidExtensionAPI $extApi, string $txt, array $extArgs |
26 | ): ?DocumentFragment { |
27 | // Drop nested refs entirely, unless we've explicitly allowed them |
28 | $parentExtTag = $extApi->parentExtTag(); |
29 | if ( $parentExtTag === 'ref' && empty( $extApi->parentExtTagOpts()['allowNestedRef'] ) ) { |
30 | return null; |
31 | } |
32 | |
33 | // The one supported case for nested refs is from the {{#tag:ref}} parser |
34 | // function. However, we're overly permissive here since we can't |
35 | // distinguish when that's nested in another template. |
36 | // The php preprocessor did our expansion. |
37 | $allowNestedRef = $extApi->inTemplate(); |
38 | |
39 | return $extApi->extTagToDOM( |
40 | $extArgs, |
41 | $txt, |
42 | [ |
43 | // NOTE: sup's content model requires it only contain phrasing |
44 | // content, not flow content. However, since we are building an |
45 | // in-memory DOM which is simply a tree data structure, we can |
46 | // nest flow content in a <sup> tag. |
47 | 'wrapperTag' => 'sup', |
48 | 'parseOpts' => [ |
49 | 'extTag' => 'ref', |
50 | 'extTagOpts' => [ 'allowNestedRef' => $allowNestedRef ], |
51 | // Ref content doesn't need p-wrapping or indent-pres. |
52 | // Treat this as inline-context content to get that b/c behavior. |
53 | 'context' => 'inline', |
54 | ], |
55 | ] |
56 | ); |
57 | } |
58 | |
59 | /** @inheritDoc */ |
60 | public function processAttributeEmbeddedHTML( |
61 | ParsoidExtensionAPI $extApi, Element $elt, Closure $proc |
62 | ): void { |
63 | $dataMw = DOMDataUtils::getDataMw( $elt ); |
64 | if ( isset( $dataMw->body->html ) ) { |
65 | $dataMw->body->html = $proc( $dataMw->body->html ); |
66 | } |
67 | } |
68 | |
69 | /** @inheritDoc */ |
70 | public function lintHandler( |
71 | ParsoidExtensionAPI $extApi, Element $ref, callable $defaultHandler |
72 | ): bool { |
73 | $dataMw = DOMDataUtils::getDataMw( $ref ); |
74 | if ( isset( $dataMw->body->html ) ) { |
75 | $fragment = $extApi->htmlToDom( $dataMw->body->html ); |
76 | $defaultHandler( $fragment ); |
77 | } elseif ( isset( $dataMw->body->id ) ) { |
78 | $refNode = DOMCompat::getElementById( $extApi->getTopLevelDoc(), $dataMw->body->id ); |
79 | if ( $refNode ) { |
80 | $defaultHandler( $refNode ); |
81 | } |
82 | } |
83 | return true; |
84 | } |
85 | |
86 | /** @inheritDoc */ |
87 | public function domToWikitext( |
88 | ParsoidExtensionAPI $extApi, Element $node, bool $wrapperUnmodified |
89 | ) { |
90 | $startTagSrc = $extApi->extStartTagToWikitext( $node ); |
91 | $dataMw = DOMDataUtils::getDataMw( $node ); |
92 | if ( !isset( $dataMw->body ) ) { |
93 | return $startTagSrc; // We self-closed this already. |
94 | } |
95 | |
96 | $html2wtOpts = [ |
97 | 'extName' => $dataMw->name, |
98 | // FIXME: One-off PHP parser state leak. This needs a better solution. |
99 | 'inPHPBlock' => true |
100 | ]; |
101 | |
102 | if ( isset( $dataMw->body->html ) ) { |
103 | // First look for the extension's content in data-mw.body.html |
104 | $src = $extApi->htmlToWikitext( $html2wtOpts, $dataMw->body->html ); |
105 | } elseif ( isset( $dataMw->body->id ) ) { |
106 | // If the body isn't contained in data-mw.body.html, look if |
107 | // there's an element pointed to by body->id. |
108 | $bodyElt = DOMCompat::getElementById( $extApi->getTopLevelDoc(), $dataMw->body->id ); |
109 | |
110 | // So far, this is specified for Cite and relies on the "id" |
111 | // referring to an element in the top level dom, even though the |
112 | // <ref> itself may be in embedded content, |
113 | // https://www.mediawiki.org/wiki/Specs/HTML/Extensions/Cite#Ref_and_References |
114 | // FIXME: This doesn't work if the <references> section |
115 | // itself is in embedded content, since we aren't traversing |
116 | // in there. |
117 | |
118 | // If we couldn't find a body element, this is a bug. |
119 | // Add some extra debugging for the editing client (ex: VisualEditor) |
120 | if ( !$bodyElt ) { |
121 | $extraDebug = ''; |
122 | $firstA = DOMCompat::querySelector( $node, 'a[href]' ); |
123 | if ( $firstA ) { |
124 | $href = DOMCompat::getAttribute( $firstA, 'href' ); |
125 | if ( $href && str_starts_with( $href, '#' ) ) { |
126 | try { |
127 | $ref = DOMCompat::querySelector( $extApi->getTopLevelDoc(), $href ); |
128 | if ( $ref ) { |
129 | $extraDebug .= ' [doc: ' . DOMCompat::getOuterHTML( $ref ) . ']'; |
130 | } |
131 | } catch ( Exception $e ) { |
132 | // We are just providing VE with debugging info. |
133 | // So, ignore all exceptions / errors in this code. |
134 | } |
135 | if ( !$extraDebug ) { |
136 | $extraDebug = ' [reference ' . $href . ' not found]'; |
137 | } |
138 | } |
139 | } |
140 | $extApi->log( |
141 | 'error/' . $dataMw->name, |
142 | 'extension src id ' . $dataMw->body->id . ' points to non-existent element for:', |
143 | DOMCompat::getOuterHTML( $node ), |
144 | '. More debug info: ', |
145 | $extraDebug |
146 | ); |
147 | return ''; // Drop it! |
148 | } |
149 | |
150 | $hasRefName = strlen( $dataMw->attrs->name ?? '' ) > 0; |
151 | $hasFollow = strlen( $dataMw->attrs->follow ?? '' ) > 0; |
152 | |
153 | if ( $hasFollow ) { |
154 | $about = DOMCompat::getAttribute( $node, 'about' ); |
155 | $followNode = $about !== null ? DOMCompat::querySelector( |
156 | $bodyElt, "span[typeof~='mw:Cite/Follow'][about='{$about}']" |
157 | ) : null; |
158 | if ( $followNode ) { |
159 | $src = $extApi->domToWikitext( $html2wtOpts, $followNode, true ); |
160 | $src = ltrim( $src, ' ' ); |
161 | } else { |
162 | $src = ''; |
163 | } |
164 | } else { |
165 | if ( $hasRefName ) { |
166 | // Follow content may have been added as spans, so drop it |
167 | if ( DOMCompat::querySelector( $bodyElt, "span[typeof~='mw:Cite/Follow']" ) ) { |
168 | $bodyElt = DOMDataUtils::cloneNode( $bodyElt, true ); |
169 | foreach ( $bodyElt->childNodes as $child ) { |
170 | if ( DOMUtils::hasTypeOf( $child, 'mw:Cite/Follow' ) ) { |
171 | // @phan-suppress-next-line PhanTypeMismatchArgumentSuperType |
172 | DOMCompat::remove( $child ); |
173 | } |
174 | } |
175 | } |
176 | } |
177 | |
178 | $src = $extApi->domToWikitext( $html2wtOpts, $bodyElt, true ); |
179 | } |
180 | } else { |
181 | $extApi->log( 'error', 'Ref body unavailable for: ' . DOMCompat::getOuterHTML( $node ) ); |
182 | return ''; // Drop it! |
183 | } |
184 | |
185 | return $startTagSrc . $src . '</' . $dataMw->name . '>'; |
186 | } |
187 | |
188 | /** @inheritDoc */ |
189 | public function diffHandler( |
190 | ParsoidExtensionAPI $extApi, callable $domDiff, Element $origNode, |
191 | Element $editedNode |
192 | ): bool { |
193 | $origDataMw = DOMDataUtils::getDataMw( $origNode ); |
194 | $editedDataMw = DOMDataUtils::getDataMw( $editedNode ); |
195 | |
196 | if ( isset( $origDataMw->body->html ) && isset( $editedDataMw->body->html ) ) { |
197 | $origFragment = $extApi->htmlToDom( |
198 | $origDataMw->body->html, $origNode->ownerDocument, |
199 | [ 'markNew' => true ] |
200 | ); |
201 | $editedFragment = $extApi->htmlToDom( |
202 | $editedDataMw->body->html, $editedNode->ownerDocument, |
203 | [ 'markNew' => true ] |
204 | ); |
205 | return call_user_func( $domDiff, $origFragment, $editedFragment ); |
206 | } elseif ( isset( $origDataMw->body->id ) && isset( $editedDataMw->body->id ) ) { |
207 | $origId = $origDataMw->body->id; |
208 | $editedId = $editedDataMw->body->id; |
209 | |
210 | // So far, this is specified for Cite and relies on the "id" |
211 | // referring to an element in the top level dom, even though the |
212 | // <ref> itself may be in embedded content, |
213 | // https://www.mediawiki.org/wiki/Specs/HTML/Extensions/Cite#Ref_and_References |
214 | // FIXME: This doesn't work if the <references> section |
215 | // itself is in embedded content, since we aren't traversing |
216 | // in there. |
217 | $origHtml = DOMCompat::getElementById( $origNode->ownerDocument, $origId ); |
218 | $editedHtml = DOMCompat::getElementById( $editedNode->ownerDocument, $editedId ); |
219 | |
220 | if ( $origHtml && $editedHtml ) { |
221 | return call_user_func( $domDiff, $origHtml, $editedHtml ); |
222 | } else { |
223 | // Log error |
224 | if ( !$origHtml ) { |
225 | $extApi->log( |
226 | 'error/domdiff/orig/ref', |
227 | "extension src id {$origId} points to non-existent element for:", |
228 | DOMCompat::getOuterHTML( $origNode ) |
229 | ); |
230 | } |
231 | if ( !$editedHtml ) { |
232 | $extApi->log( |
233 | // use info level to avoid logspam for CX edits where translated |
234 | // docs might reference nodes not copied over from orig doc. |
235 | 'info/domdiff/edited/ref', |
236 | "extension src id {$editedId} points to non-existent element for:", |
237 | DOMCompat::getOuterHTML( $editedNode ) |
238 | ); |
239 | } |
240 | } |
241 | } |
242 | |
243 | // FIXME: Similar to DOMDiff::subtreeDiffers, maybe $editNode should |
244 | // be marked as inserted to avoid losing any edits, at the cost of |
245 | // more normalization |
246 | |
247 | return false; |
248 | } |
249 | } |