Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 94
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
ParsoidTagHandler
0.00% covered (danger)
0.00%
0 / 94
0.00% covered (danger)
0.00%
0 / 6
812
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 parseTag
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
30
 processArguments
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 reportErrors
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
56
 getJSONValidatorLog
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
12
 processAttributeEmbeddedDom
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
132
1<?php
2
3namespace Kartographer\Tag;
4
5use Kartographer\ParsoidWikitextParser;
6use Kartographer\SimpleStyleParser;
7use LogicException;
8use MediaWiki\Config\Config;
9use MediaWiki\Language\LanguageFactory;
10use MediaWiki\Language\LanguageNameUtils;
11use MediaWiki\Message\Message;
12use StatusValue;
13use stdClass;
14use Wikimedia\Parsoid\Config\DataAccess;
15use Wikimedia\Parsoid\DOM\DocumentFragment;
16use Wikimedia\Parsoid\DOM\Element;
17use Wikimedia\Parsoid\Ext\ExtensionTagHandler;
18use Wikimedia\Parsoid\Ext\ParsoidExtensionAPI;
19use Wikimedia\Parsoid\Utils\DOMCompat;
20
21/**
22 * @license MIT
23 */
24abstract class ParsoidTagHandler extends ExtensionTagHandler {
25    public const TAG = '';
26
27    public function __construct(
28        protected readonly Config $config,
29        private readonly LanguageFactory $languageFactory,
30        private readonly LanguageNameUtils $languageNameUtils,
31        protected readonly DataAccess $dataAccess,
32    ) {
33    }
34
35    /**
36     * @param ParsoidExtensionAPI $extApi
37     * @param string $input
38     * @param array $extArgs
39     * @return array{StatusValue,MapTagArgumentValidator,stdClass[]}
40     */
41    protected function parseTag( ParsoidExtensionAPI $extApi, string $input, array $extArgs ): array {
42        $args = $this->processArguments(
43            $extApi->extArgsToArray( $extArgs ),
44            $extApi
45        );
46        $status = $args->status;
47
48        $geometries = [];
49        if ( $status->isOK() ) {
50            $wp = new ParsoidWikitextParser( $extApi );
51            $status = ( new SimpleStyleParser( $wp, $this->config ) )->parse( $input );
52            if ( $status->isOk() ) {
53                $geometries = $status->getValue()['data'];
54            }
55        }
56
57        if ( $geometries ) {
58            $marker = SimpleStyleParser::findFirstMarkerSymbol( $geometries );
59            if ( $marker ) {
60                $args->setFirstMarkerProperties( ...$marker );
61            }
62        }
63
64        return [ $status, $args, $geometries ];
65    }
66
67    /**
68     * @param array<string,string> $args
69     * @param ParsoidExtensionAPI $extApi
70     * @return MapTagArgumentValidator
71     */
72    private function processArguments(
73        array $args,
74        ParsoidExtensionAPI $extApi
75    ): MapTagArgumentValidator {
76        return new MapTagArgumentValidator( static::TAG, $args,
77            $this->config,
78            // TODO Ideally, this wouldn't need the page language, and it would generate an href with an i18n'd
79            // attribute that would then get localized. But, this would require rich attributes to do cleanly, so
80            // let's punt that to later.
81            $this->languageFactory->getLanguage( $extApi->getPageConfig()->getPageLanguageBcp47() ),
82            $this->languageNameUtils
83        );
84    }
85
86    /**
87     * @param ParsoidExtensionAPI $extApi
88     * @param string $tag
89     * @param StatusValue<array> $status
90     * @return DocumentFragment
91     */
92    public function reportErrors(
93        ParsoidExtensionAPI $extApi, string $tag, StatusValue $status
94    ): DocumentFragment {
95        $errors = $status->getErrors();
96        if ( !$errors ) {
97            throw new LogicException( __METHOD__ . '(): attempt to report error when none took place' );
98        }
99        $dom = $extApi->getTopLevelDoc()->createDocumentFragment();
100        $div = $extApi->getTopLevelDoc()->createElement( 'div' );
101        $div->setAttribute( 'class', 'mw-kartographer-error' );
102        $div->setAttribute( 'data-mw-kartographer', $tag );
103        $div->setAttribute( 'data-kart', 'error' );
104        $dom->appendChild( $div );
105        if ( count( $errors ) > 1 ) {
106            // kartographer-error-context-multi takes two parameters: the tag name and the
107            // (formatted, localized) list of errors. We pass '' as second parameter and add the
108            // individual errors to the fragment instead.
109            $errContainer = $extApi->createInterfaceI18nFragment( 'kartographer-error-context-multi',
110                [ static::TAG, '' ] );
111            $div->appendChild( $errContainer );
112            $ul = $extApi->getTopLevelDoc()->createElement( 'ul' );
113            $div->appendChild( $ul );
114            foreach ( $errors as $err ) {
115                $li = $extApi->getTopLevelDoc()->createElement( 'li' );
116                $ul->appendChild( $li );
117                $err = $extApi->createInterfaceI18nFragment( $err['message'], $err['params'] );
118                $li->appendChild( $err );
119            }
120        } else {
121            // kartographer-error-context takes two parameters: the tag name and the
122            // localized error. We pass '' as second parameter and add the
123            // individual errors to the fragment instead.
124            $errContainer = $extApi->createInterfaceI18nFragment( 'kartographer-error-context', [ static::TAG, '' ] );
125            $div->appendChild( $errContainer );
126            $params = [];
127            foreach ( $errors[0]['params'] as $k => $p ) {
128                if ( $p instanceof Message ) {
129                    $params[$k] = $p->toString( Message::FORMAT_PARSE );
130                } else {
131                    $params[$k] = $p;
132                }
133            }
134            $err = $extApi->createInterfaceI18nFragment( $errors[0]['message'], $params );
135            $div->appendChild( $err );
136        }
137
138        $jsonErrors = $this->getJSONValidatorLog( $extApi, $status );
139
140        if ( $jsonErrors ) {
141            $div->appendChild( $jsonErrors );
142        }
143        return $dom;
144    }
145
146    /**
147     * @param ParsoidExtensionAPI $extApi
148     * @param StatusValue<array> $status
149     * @return ?DocumentFragment
150     */
151    private function getJSONValidatorLog( ParsoidExtensionAPI $extApi, StatusValue $status ): ?DocumentFragment {
152        $errors = $status->getValue()['schema-errors'] ?? [];
153        if ( !$errors ) {
154            return null;
155        }
156
157        $dom = $extApi->getTopLevelDoc()->createDocumentFragment();
158        $ul = $extApi->getTopLevelDoc()->createElement( 'ul' );
159        $ul->setAttribute( 'class', 'mw-kartographer-error-log mw-collapsible mw-collapsed' );
160        $dom->appendChild( $ul );
161        /** These errors come from {@see \JsonSchema\Constraints\BaseConstraint::addError} */
162        foreach ( $errors as $error ) {
163            $li = $extApi->getTopLevelDoc()->createElement( 'li' );
164            $pointer = $extApi->getTopLevelDoc()->createTextNode( $error['pointer'] );
165            $sep = $extApi->createInterfaceI18nFragment( 'colon-separator', [] );
166            $msg = $extApi->getTopLevelDoc()->createTextNode( $error['message'] );
167            $li->appendChild( $pointer );
168            $li->appendChild( $sep );
169            $li->appendChild( $msg );
170            $ul->appendChild( $li );
171        }
172        return $dom;
173    }
174
175    public function processAttributeEmbeddedDom( ParsoidExtensionAPI $extApi, Element $elt, callable $proc ): void {
176        if ( $elt->hasAttribute( 'data-mw-kartographer' ) ) {
177            $node = $elt;
178        } else {
179            $node = DOMCompat::querySelector( $elt, '*[data-mw-kartographer]' );
180        }
181        if ( !$node ) {
182            return;
183        }
184        $exttagname = $node->getAttribute( 'data-mw-kartographer' );
185        if ( !$exttagname ) {
186            return;
187        }
188        $marker = json_decode( $node->getAttribute( 'data-kart' ) ?? '', false );
189        if ( $marker instanceof stdClass && $marker->geometries ) {
190            foreach ( $marker->geometries as $geom ) {
191                if ( !isset( $geom->properties ) ) {
192                    continue;
193                }
194                foreach ( $geom->properties as $key => $prop ) {
195                    if ( in_array( $key, SimpleStyleParser::WIKITEXT_PROPERTIES ) ) {
196                        $df = $extApi->htmlToDom( $prop );
197                        $changed = $proc( $df );
198                        if ( $changed ) {
199                            $geom->properties->{$key} = $extApi->domToHtml(
200                                $df, releaseDom: true
201                            );
202                        }
203                    }
204                }
205            }
206            $node->setAttribute( 'data-kart', json_encode( $marker ) );
207        }
208    }
209}