Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 98 |
|
0.00% |
0 / 6 |
CRAP | |
0.00% |
0 / 1 |
ParsoidDomProcessor | |
0.00% |
0 / 98 |
|
0.00% |
0 / 6 |
1056 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
wtPostprocess | |
0.00% |
0 / 37 |
|
0.00% |
0 / 1 |
90 | |||
processKartographerNode | |
0.00% |
0 / 38 |
|
0.00% |
0 / 1 |
272 | |||
updateSrc | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
updateSrcSet | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
updateUrl | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace Kartographer\Tag; |
4 | |
5 | use Kartographer\ParsoidUtils; |
6 | use Kartographer\SimpleStyleParser; |
7 | use MediaWiki\Config\Config; |
8 | use MediaWiki\Json\FormatJson; |
9 | use MediaWiki\Parser\ParserOutputStringSets; |
10 | use MediaWiki\Title\Title; |
11 | use stdClass; |
12 | use Wikimedia\Parsoid\DOM\Element; |
13 | use Wikimedia\Parsoid\DOM\Node; |
14 | use Wikimedia\Parsoid\Ext\DOMDataUtils; |
15 | use Wikimedia\Parsoid\Ext\DOMProcessor; |
16 | use Wikimedia\Parsoid\Ext\ParsoidExtensionAPI; |
17 | use Wikimedia\Parsoid\Utils\DOMCompat; |
18 | use Wikimedia\Parsoid\Utils\DOMTraverser; |
19 | |
20 | /** |
21 | * @license MIT |
22 | */ |
23 | class ParsoidDomProcessor extends DOMProcessor { |
24 | |
25 | private Config $config; |
26 | |
27 | public function __construct( |
28 | Config $config |
29 | ) { |
30 | $this->config = $config; |
31 | } |
32 | |
33 | /** @inheritDoc */ |
34 | public function wtPostprocess( ParsoidExtensionAPI $extApi, Node $root, array $options ): void { |
35 | if ( !( $root instanceof Element ) ) { |
36 | return; |
37 | } |
38 | |
39 | $state = [ |
40 | 'broken' => 0, |
41 | 'interactiveGroups' => [], |
42 | 'requestedGroups' => [], |
43 | 'counters' => [], |
44 | 'maplinks' => 0, |
45 | 'mapframes' => 0, |
46 | 'data' => [], |
47 | ]; |
48 | |
49 | // FIXME This only selects data-mw-kartographer nodes without exploring HTML that may be stored in |
50 | // attributes. We need to expand the traversal to find these as well. |
51 | $kartnodes = DOMCompat::querySelectorAll( $root, '*[data-mw-kartographer]' ); |
52 | |
53 | // let's avoid adding data to the page if there's no kartographer nodes! |
54 | if ( !$kartnodes ) { |
55 | return; |
56 | } |
57 | |
58 | $mapServer = $this->config->get( 'KartographerMapServer' ); |
59 | $extApi->getMetadata()->addModuleStyles( [ 'ext.kartographer.style' ] ); |
60 | $extApi->getMetadata()->appendOutputStrings( ParserOutputStringSets::EXTRA_CSP_DEFAULT_SRC, [ $mapServer ] ); |
61 | |
62 | $traverser = new DOMTraverser( false, true ); |
63 | $traverser->addHandler( null, function ( $node ) use ( &$state, $extApi ) { |
64 | if ( $node instanceof Element && $node->hasAttribute( 'data-mw-kartographer' ) ) { |
65 | $this->processKartographerNode( $node, $extApi, $state ); |
66 | } |
67 | return true; |
68 | } ); |
69 | $traverser->traverse( $extApi, $root ); |
70 | |
71 | if ( $state['broken'] > 0 ) { |
72 | ParsoidUtils::addCategory( $extApi, 'kartographer-broken-category' ); |
73 | } |
74 | if ( $state['maplinks'] + $state['mapframes'] > $state['broken'] ) { |
75 | ParsoidUtils::addCategory( $extApi, 'kartographer-tracking-category' ); |
76 | } |
77 | |
78 | $interactive = $state['interactiveGroups']; |
79 | $state['interactiveGroups'] = array_keys( $state['interactiveGroups'] ); |
80 | $state['requestedGroups'] = array_keys( $state['requestedGroups'] ); |
81 | $state['parsoidIntVersion'] = $this->config->get( 'KartographerParsoidVersion' ); |
82 | $extApi->getMetadata()->setExtensionData( 'kartographer', $state ); |
83 | |
84 | foreach ( $interactive as $req ) { |
85 | if ( !isset( $state['data'][$req] ) ) { |
86 | $state['data'][$req] = []; |
87 | } |
88 | } |
89 | $extApi->getMetadata()->setJsConfigVar( 'wgKartographerLiveData', $state['data'] ?? [] ); |
90 | } |
91 | |
92 | private function processKartographerNode( Element $kartnode, ParsoidExtensionAPI $extApi, array &$state ): void { |
93 | $tagName = $kartnode->getAttribute( 'data-mw-kartographer' ) ?? ''; |
94 | if ( $tagName !== '' ) { |
95 | $state[$tagName . 's' ]++; |
96 | } |
97 | |
98 | $markerStr = $kartnode->getAttribute( 'data-kart' ); |
99 | $kartnode->removeAttribute( 'data-kart' ); |
100 | if ( $markerStr === 'error' ) { |
101 | $state['broken']++; |
102 | return; |
103 | } |
104 | $marker = json_decode( $markerStr ?? '' ); |
105 | |
106 | $state['requestedGroups'] = array_merge( $state['requestedGroups'], $marker->showGroups ?? [] ); |
107 | if ( $tagName === ParsoidMapFrame::TAG ) { |
108 | $state['interactiveGroups'] = array_merge( $state['interactiveGroups'], $marker->showGroups ?? [] ); |
109 | } |
110 | |
111 | if ( !$marker || !$marker->geometries || !$marker->geometries[0] instanceof stdClass ) { |
112 | return; |
113 | } |
114 | [ $counter, $props ] = SimpleStyleParser::updateMarkerSymbolCounters( $marker->geometries, |
115 | $state['counters'] ); |
116 | if ( $tagName === ParsoidMapLink::TAG && $counter ) { |
117 | if ( !isset( DOMDataUtils::getDataMw( $kartnode )->attrs->text ) ) { |
118 | $text = $extApi->getTopLevelDoc()->createTextNode( $counter ); |
119 | $kartnode->replaceChild( $text, $kartnode->firstChild ); |
120 | } |
121 | } |
122 | |
123 | $data = $marker->geometries; |
124 | |
125 | if ( $counter ) { |
126 | // If we have a counter, we update the marker data prior to encoding the groupId, and we remove |
127 | // the (previously computed) groupId from showGroups |
128 | if ( ( $marker->groupId[0] ?? '' ) === '_' ) { |
129 | // TODO unclear if this is necessary or if we could simply set it to []. |
130 | $marker->showGroups = array_values( array_diff( $marker->showGroups, [ $marker->groupId ] ) ); |
131 | $marker->groupId = null; |
132 | } |
133 | } |
134 | |
135 | $groupId = $marker->groupId ?? null; |
136 | if ( $groupId === null ) { |
137 | // This hash calculation MUST be the same as in LegacyTagHandler::updateState |
138 | $groupId = '_' . sha1( FormatJson::encode( $marker->geometries, false, FormatJson::ALL_OK ) ); |
139 | $marker->groupId = $groupId; |
140 | $marker->showGroups[] = $groupId; |
141 | $kartnode->setAttribute( 'data-overlays', FormatJson::encode( $marker->showGroups ) ); |
142 | $img = $kartnode->firstChild; |
143 | // this should always be the case, but let make phan aware of it |
144 | if ( $img instanceof Element ) { |
145 | $this->updateSrc( $img, $groupId, $extApi ); |
146 | $this->updateSrcSet( $img, $groupId, $extApi ); |
147 | } |
148 | } |
149 | |
150 | // There is no way to ever add anything to a private group starting with `_` |
151 | if ( isset( $state['data'][$groupId] ) && !str_starts_with( $groupId, '_' ) ) { |
152 | $state['data'][$groupId] = array_merge( $state['data'][$groupId], $data ); |
153 | } else { |
154 | $state['data'][$groupId] = $data; |
155 | } |
156 | } |
157 | |
158 | private function updateSrc( Element $firstChild, string $groupId, ParsoidExtensionAPI $extApi ): void { |
159 | $src = $firstChild->getAttribute( 'src' ) ?? ''; |
160 | if ( $src !== '' ) { |
161 | $src = $this->updateUrl( $src, $extApi, $groupId ); |
162 | $firstChild->setAttribute( 'src', $src ); |
163 | } |
164 | } |
165 | |
166 | private function updateSrcSet( Element $firstChild, string $groupId, ParsoidExtensionAPI $extApi ): void { |
167 | $srcset = $firstChild->getAttribute( 'srcset' ) ?? ''; |
168 | if ( $srcset !== '' ) { |
169 | $arr = explode( ', ', $srcset ); |
170 | $srcsets = []; |
171 | foreach ( $arr as $plop ) { |
172 | $toks = explode( ' ', $plop ); |
173 | $toks[0] = $this->updateUrl( $toks[0], $extApi, $groupId ); |
174 | $srcsets[] = implode( ' ', $toks ); |
175 | } |
176 | $firstChild->setAttribute( 'srcset', implode( ', ', $srcsets ) ); |
177 | } |
178 | } |
179 | |
180 | private function updateUrl( string $src, ParsoidExtensionAPI $extApi, string $groupId ): string { |
181 | $url = explode( '?', $src ); |
182 | $attrs = wfCgiToArray( $url[1] ); |
183 | |
184 | $linkTarget = $extApi->getPageConfig()->getLinkTarget(); |
185 | $pagetitle = Title::newFromLinkTarget( $linkTarget )->getPrefixedDBkey(); |
186 | $revisionId = $extApi->getPageConfig()->getRevisionId(); |
187 | $attrs = array_merge( $attrs, |
188 | MapFrameAttributeGenerator::getUrlAttrs( $this->config, $pagetitle, $revisionId, [ $groupId ] ) |
189 | ); |
190 | |
191 | return $url[0] . '?' . wfArrayToCgi( $attrs ); |
192 | } |
193 | |
194 | } |