Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 91
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
ParsoidTagHandler
0.00% covered (danger)
0.00%
0 / 91
0.00% covered (danger)
0.00%
0 / 5
930
0.00% covered (danger)
0.00%
0 / 1
 parseTag
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
56
 processParsoidExtensionArguments
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 reportErrors
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
30
 getJSONValidatorLog
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
12
 processAttributeEmbeddedHTML
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
110
1<?php
2
3namespace Kartographer\Tag;
4
5use Closure;
6use Kartographer\ParsoidWikitextParser;
7use Kartographer\SimpleStyleParser;
8use LogicException;
9use MediaWiki\MediaWikiServices;
10use StatusValue;
11use stdClass;
12use Wikimedia\Parsoid\DOM\DocumentFragment;
13use Wikimedia\Parsoid\DOM\Element;
14use Wikimedia\Parsoid\Ext\ExtensionTagHandler;
15use Wikimedia\Parsoid\Ext\ParsoidExtensionAPI;
16use Wikimedia\Parsoid\Ext\Utils;
17use Wikimedia\Parsoid\Tokens\KVSourceRange;
18use Wikimedia\Parsoid\Utils\DOMCompat;
19
20/**
21 * @license MIT
22 */
23class ParsoidTagHandler extends ExtensionTagHandler {
24    public const TAG = '';
25
26    /**
27     * @param ParsoidExtensionAPI $extApi
28     * @param string $input
29     * @param stdClass[] $extArgs
30     * @return array{StatusValue,MapTagArgumentValidator,stdClass[],KVSourceRange[]}
31     */
32    protected function parseTag( ParsoidExtensionAPI $extApi, string $input, array $extArgs ): array {
33        $args = $this->processParsoidExtensionArguments( $extArgs );
34        $status = $args->status;
35        $srcOffsets = [];
36        foreach ( $extArgs as $extArg ) {
37            if ( is_string( $extArg->k ) ) {
38                $srcOffsets[$extArg->k] = $extArg->srcOffsets ?? null;
39            }
40        }
41
42        $geometries = [];
43        if ( $status->isOK() ) {
44            $wp = new ParsoidWikitextParser( $extApi );
45            $status = ( new SimpleStyleParser( $wp ) )->parse( $input );
46            if ( $status->isOk() ) {
47                $geometries = $status->getValue()['data'];
48            }
49        }
50
51        if ( $geometries ) {
52            $marker = SimpleStyleParser::findFirstMarkerSymbol( $geometries );
53            if ( $marker ) {
54                $args->setFirstMarkerProperties( ...$marker );
55            }
56        }
57
58        return [ $status, $args, $geometries, $srcOffsets ];
59    }
60
61    /**
62     * @param stdClass[] $extArgs
63     * @return MapTagArgumentValidator
64     */
65    private function processParsoidExtensionArguments( array $extArgs ): MapTagArgumentValidator {
66        $services = MediaWikiServices::getInstance();
67
68        $args = [];
69        foreach ( $extArgs as $extArg ) {
70            // Might be an array or Token object when wikitext like <maplink {{1x|text}}=… /> is
71            // used. The old parser doesn't resolve this either.
72            if ( is_string( $extArg->k ) ) {
73                if ( $extArg->k === 'text' && $extArg->vsrc ) {
74                    // preserve newlines and spaces for 'text' attribute
75                    $args[$extArg->k] = Utils::decodeWtEntities( $extArg->vsrc );
76                } else {
77                    $args[$extArg->k] = $extArg->v;
78                }
79            }
80        }
81
82        return new MapTagArgumentValidator( static::TAG, $args,
83            $services->getMainConfig(),
84            // FIXME setting the display language to English for the first version, needs to be fixed when we
85            // have a localization solution for Parsoid
86            $services->getLanguageFactory()->getLanguage( 'en' ),
87            $services->getLanguageNameUtils()
88        );
89    }
90
91    /**
92     * @param ParsoidExtensionAPI $extApi
93     * @param string $tag
94     * @param StatusValue $status
95     * @return DocumentFragment
96     */
97    public function reportErrors(
98        ParsoidExtensionAPI $extApi, string $tag, StatusValue $status
99    ): DocumentFragment {
100        $errors = $status->getErrors();
101        if ( !$errors ) {
102            throw new LogicException( __METHOD__ . '(): attempt to report error when none took place' );
103        }
104        $dom = $extApi->getTopLevelDoc()->createDocumentFragment();
105        $div = $extApi->getTopLevelDoc()->createElement( 'div' );
106        $div->setAttribute( 'class', 'mw-kartographer-error' );
107        $div->setAttribute( 'data-mw-kartographer', $tag );
108        $div->setAttribute( 'data-kart', 'error' );
109        $dom->appendChild( $div );
110        if ( count( $errors ) > 1 ) {
111            // kartographer-error-context-multi takes two parameters: the tag name and the
112            // (formatted, localized) list of errors. We pass '' as second parameter and add the
113            // individual errors to the fragment instead.
114            $errContainer = $extApi->createInterfaceI18nFragment( 'kartographer-error-context-multi',
115                [ static::TAG, '' ] );
116            $div->appendChild( $errContainer );
117            $ul = $extApi->getTopLevelDoc()->createElement( 'ul' );
118            $div->appendChild( $ul );
119            foreach ( $errors as $err ) {
120                $li = $extApi->getTopLevelDoc()->createElement( 'li' );
121                $ul->appendChild( $li );
122                $err = $extApi->createInterfaceI18nFragment( $err['message'], $err['params'] );
123                $li->appendChild( $err );
124            }
125        } else {
126            // kartographer-error-context takes two parameters: the tag name and the
127            // localized error. We pass '' as second parameter and add the
128            // individual errors to the fragment instead.
129            $errContainer = $extApi->createInterfaceI18nFragment( 'kartographer-error-context', [ static::TAG, '' ] );
130            $div->appendChild( $errContainer );
131            $err = $extApi->createInterfaceI18nFragment( $errors[0]['message'], $errors[0]['params'] );
132            $div->appendChild( $err );
133        }
134
135        $jsonErrors = $this->getJSONValidatorLog( $extApi, $status );
136
137        if ( $jsonErrors ) {
138            $div->appendChild( $jsonErrors );
139        }
140        return $dom;
141    }
142
143    /**
144     * @param ParsoidExtensionAPI $extApi
145     * @param StatusValue $status
146     * @return ?DocumentFragment
147     */
148    private function getJSONValidatorLog( ParsoidExtensionAPI $extApi, StatusValue $status ): ?DocumentFragment {
149        $errors = $status->getValue()['schema-errors'] ?? [];
150        if ( !$errors ) {
151            return null;
152        }
153
154        $dom = $extApi->getTopLevelDoc()->createDocumentFragment();
155        $ul = $extApi->getTopLevelDoc()->createElement( 'ul' );
156        $ul->setAttribute( 'class', 'mw-kartographer-error-log mw-collapsible mw-collapsed' );
157        $dom->appendChild( $ul );
158        /** These errors come from {@see \JsonSchema\Constraints\BaseConstraint::addError} */
159        foreach ( $errors as $error ) {
160            $li = $extApi->getTopLevelDoc()->createElement( 'li' );
161            $pointer = $extApi->getTopLevelDoc()->createTextNode( $error['pointer'] );
162            $sep = $extApi->createInterfaceI18nFragment( 'colon-separator', [] );
163            $msg = $extApi->getTopLevelDoc()->createTextNode( $error['message'] );
164            $li->appendChild( $pointer );
165            $li->appendChild( $sep );
166            $li->appendChild( $msg );
167            $ul->appendChild( $li );
168        }
169        return $dom;
170    }
171
172    public function processAttributeEmbeddedHTML( ParsoidExtensionAPI $extApi, Element $elt, Closure $proc ): void {
173        if ( $elt->hasAttribute( 'data-mw-kartographer' ) ) {
174            $node = $elt;
175        } else {
176            $node = DOMCompat::querySelector( $elt, '*[data-mw-kartographer]' );
177        }
178        if ( !$node ) {
179            return;
180        }
181        $exttagname = $node->getAttribute( 'data-mw-kartographer' );
182        if ( !$exttagname ) {
183            return;
184        }
185        $marker = json_decode( $node->getAttribute( 'data-kart' ) ?? '', false );
186        if ( $marker instanceof stdClass && $marker->geometries ) {
187            foreach ( $marker->geometries as $geom ) {
188                if ( !isset( $geom->properties ) ) {
189                    continue;
190                }
191                foreach ( $geom->properties as $key => $prop ) {
192                    if ( in_array( $key, SimpleStyleParser::WIKITEXT_PROPERTIES ) ) {
193                        $geom->properties->{$key} = $proc( $prop );
194                    }
195                }
196            }
197            $node->setAttribute( 'data-kart', json_encode( $marker ) );
198        }
199    }
200}