Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 100
0.00% covered (danger)
0.00%
0 / 21
CRAP
0.00% covered (danger)
0.00%
0 / 1
Token
0.00% covered (danger)
0.00%
0 / 100
0.00% covered (danger)
0.00%
0 / 21
1980
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 __clone
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 jsonSerialize
n/a
0 / 0
n/a
0 / 0
0
 toJsonArray
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 jsonClassHintFor
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 newFromJsonArray
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 hint
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getType
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 addAttribute
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addNormalizedAttribute
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getAttributeV
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getAttributeKV
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 hasAttribute
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setAttribute
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 setShadowInfo
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 setShadowInfoIfModified
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
12
 getAttributeShadowInfo
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
42
 removeAttribute
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
 addSpaceSeparatedAttribute
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getWTSource
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getToken
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 fetchExpandedAttrValue
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\Parsoid\Tokens;
5
6use Wikimedia\Assert\Assert;
7use Wikimedia\JsonCodec\Hint;
8use Wikimedia\JsonCodec\JsonCodecable;
9use Wikimedia\JsonCodec\JsonCodecableTrait;
10use Wikimedia\Parsoid\Core\Source;
11use Wikimedia\Parsoid\Core\SourceRange;
12use Wikimedia\Parsoid\DOM\DocumentFragment;
13use Wikimedia\Parsoid\NodeData\DataMw;
14use Wikimedia\Parsoid\NodeData\DataParsoid;
15use Wikimedia\Parsoid\Utils\CompatJsonCodec;
16use Wikimedia\Parsoid\Utils\Utils;
17
18/**
19 * Catch-all class for all token types.
20 */
21abstract class Token implements JsonCodecable, \JsonSerializable {
22    use JsonCodecableTrait;
23
24    public DataParsoid $dataParsoid;
25    public ?DataMw $dataMw = null;
26
27    /** @var ?array<KV> */
28    public ?array $attribs = null;
29
30    protected function __construct(
31        ?DataParsoid $dataParsoid, ?DataMw $dataMw
32    ) {
33        $this->dataParsoid = $dataParsoid ?? new DataParsoid;
34        $this->dataMw = $dataMw;
35    }
36
37    public function __clone() {
38        // Deep clone non-primitive properties
39        $this->dataParsoid = clone $this->dataParsoid;
40        if ( $this->dataMw !== null ) {
41            $this->dataMw = clone $this->dataMw;
42        }
43        if ( $this->attribs !== null ) {
44            $this->attribs = Utils::cloneArray( $this->attribs );
45        }
46    }
47
48    /**
49     * @inheritDoc
50     */
51    #[\ReturnTypeWillChange]
52    abstract public function jsonSerialize();
53
54    /** @inheritDoc */
55    public function toJsonArray(): array {
56        return $this->jsonSerialize();
57    }
58
59    /** @inheritDoc */
60    public static function jsonClassHintFor( string $keyName ) {
61        return match ( $keyName ) {
62            'dataParsoid' => DataParsoid::hint(),
63            'dataMw' => DataMw::hint(),
64            'attribs' => Hint::build( KV::class, Hint::LIST ),
65            'nestedTokens' => new Hint( self::hint(), Hint::LIST ),
66            default => null
67        };
68    }
69
70    /** @inheritDoc */
71    public static function newFromJsonArray( array $json ) {
72        $type = $json['type'] ?? '\\';
73        Assert::invariant( !str_contains( $type, '\\' ), 'Bad type' );
74        $classParts = explode( '\\', self::class );
75        array_pop( $classParts );
76        $type = implode( '\\', [ ...$classParts, $type ] );
77        Assert::invariant( $type !== self::class, 'Bad type' );
78        return $type::newFromJsonArray( $json );
79    }
80
81    public static function hint(): Hint {
82        return Hint::build( self::class, Hint::INHERITED );
83    }
84
85    /**
86     * Returns a string key for this token
87     * @return string
88     */
89    public function getType(): string {
90        $classParts = explode( '\\', get_class( $this ) );
91        return end( $classParts );
92    }
93
94    /**
95     * Generic set attribute method.
96     *
97     * @param string $name
98     *    Always a string when used this way.
99     *    The more complex form (where the key is a non-string) are found when
100     *    KV objects are constructed in the tokenizer.
101     * @param string|Token|array<Token|string> $value
102     * @param ?KVSourceRange $srcOffsets
103     */
104    public function addAttribute(
105        string $name, $value, ?KVSourceRange $srcOffsets = null
106    ): void {
107        $this->attribs[] = new KV( $name, $value, $srcOffsets );
108    }
109
110    /**
111     * Generic set attribute method with support for change detection.
112     * Set a value and preserve the original wikitext that produced it.
113     *
114     * @param string $name
115     * @param string|Token|array<Token|string> $value
116     * @param mixed $origValue
117     */
118    public function addNormalizedAttribute( string $name, $value, $origValue ): void {
119        $this->addAttribute( $name, $value );
120        $this->setShadowInfoIfModified( $name, $value, $origValue );
121    }
122
123    /**
124     * Generic attribute accessor.
125     *
126     * @param string $name
127     * @return string|Token|array<Token|string>|KV[]|null
128     */
129    public function getAttributeV( string $name ) {
130        return KV::lookup( $this->attribs, $name );
131    }
132
133    /**
134     * Generic attribute accessor.
135     *
136     * @param string $name
137     * @return KV|null
138     */
139    public function getAttributeKV( string $name ) {
140        return KV::lookupKV( $this->attribs, $name );
141    }
142
143    /**
144     * Generic attribute accessor.
145     *
146     * @param string $name
147     * @return bool
148     */
149    public function hasAttribute( string $name ): bool {
150        return $this->getAttributeKV( $name ) !== null;
151    }
152
153    /**
154     * Set an unshadowed attribute.
155     *
156     * @param string $name
157     * @param string|Token|array<Token|string> $value
158     */
159    public function setAttribute( string $name, $value ): void {
160        // First look for the attribute and change the last match if found.
161        for ( $i = count( $this->attribs ) - 1; $i >= 0; $i-- ) {
162            $kv = $this->attribs[$i];
163            $k = $kv->k;
164            if ( is_string( $k ) && mb_strtolower( $k ) === $name ) {
165                $kv->v = $value;
166                $this->attribs[$i] = $kv;
167                return;
168            }
169        }
170        // Nothing found, just add the attribute
171        $this->addAttribute( $name, $value );
172    }
173
174    /**
175     * Store the original value of an attribute in a token's dataParsoid.
176     *
177     * @param string $name
178     * @param mixed $value
179     * @param mixed $origValue
180     */
181    public function setShadowInfo( string $name, $value, $origValue ): void {
182        $this->dataParsoid->a ??= [];
183        $this->dataParsoid->a[$name] = $value;
184        $this->dataParsoid->sa ??= [];
185        $this->dataParsoid->sa[$name] = $origValue;
186    }
187
188    /**
189     * Store the original value of an attribute in a token's dataParsoid.
190     *
191     * @param string $name
192     * @param mixed $value
193     * @param mixed $origValue
194     */
195    public function setShadowInfoIfModified( string $name, $value, $origValue ): void {
196        // Don't shadow if value is the same or the orig is null
197        if ( $value !== $origValue && $origValue !== null ) {
198            $this->setShadowInfo( $name, $value, $origValue );
199        }
200    }
201
202    /**
203     * Attribute info accessor for the wikitext serializer. Performs change
204     * detection and uses unnormalized attribute values if set. Expects the
205     * context to be set to a token.
206     *
207     * @param string $name
208     * @return array{value: string|Token|array<Token|KV|string>, modified: bool, fromsrc: bool}
209     *  Information about the shadow info attached to this attribute:
210     *   - value: (string|Token|array<Token|KV|string>)
211     *     When modified is false and fromsrc is true, this is always a string.
212     *   - modified: (bool)
213     *   - fromsrc: (bool)
214     */
215    public function getAttributeShadowInfo( string $name ): array {
216        $curVal = $this->getAttributeV( $name );
217
218        // Not the case, continue regular round-trip information.
219        if ( !property_exists( $this->dataParsoid, 'a' ) ||
220            !array_key_exists( $name, $this->dataParsoid->a )
221        ) {
222            return [
223                "value" => $curVal,
224                // Mark as modified if a new element
225                "modified" => $this->dataParsoid->isEmpty(),
226                "fromsrc" => false
227            ];
228        } elseif ( $this->dataParsoid->a[$name] !== $curVal ) {
229            return [
230                "value" => $curVal,
231                "modified" => true,
232                "fromsrc" => false
233            ];
234        } elseif ( !property_exists( $this->dataParsoid, 'sa' ) ||
235            !array_key_exists( $name, $this->dataParsoid->sa )
236        ) {
237            return [
238                "value" => $curVal,
239                "modified" => false,
240                "fromsrc" => false
241            ];
242        } else {
243            return [
244                "value" => $this->dataParsoid->sa[$name],
245                "modified" => false,
246                "fromsrc" => true
247            ];
248        }
249    }
250
251    /**
252     * Completely remove all attributes with this name.
253     *
254     * @param string $name
255     */
256    public function removeAttribute( string $name ): void {
257        foreach ( $this->attribs as $i => $kv ) {
258            if ( is_string( $kv->k ) && mb_strtolower( $kv->k ) === $name ) {
259                unset( $this->attribs[$i] );
260            }
261        }
262        $this->attribs = array_values( $this->attribs );
263    }
264
265    /**
266     * Add a space-separated property value.
267     * These are Parsoid-added attributes, not something present in source.
268     * So, only a regular ASCII space characters will be used here.
269     *
270     * @param string $name The attribute name
271     * @param string $value The value to add to the attribute
272     */
273    public function addSpaceSeparatedAttribute( string $name, string $value ): void {
274        $curVal = $this->getAttributeKV( $name );
275        if ( $curVal !== null ) {
276            if ( in_array( $value, explode( ' ', $curVal->v ), true ) ) {
277                // value is already included, nothing to do.
278                return;
279            }
280
281            // Value was not yet included in the existing attribute, just add
282            // it separated with a space
283            $this->setAttribute( $curVal->k, $curVal->v . ' ' . $value );
284        } else {
285            // the attribute did not exist at all, just add it
286            $this->addAttribute( $name, $value );
287        }
288    }
289
290    /**
291     * Get the wikitext source of a token.
292     *
293     * @param Source ...$source Optional Source, for context.
294     * @return string
295     */
296    public function getWTSource( Source ...$source ): string {
297        $tsr = $this->dataParsoid->tsr ?? null;
298        if ( !( $tsr instanceof SourceRange ) ) {
299            throw new InvalidTokenException( 'Expected token to have tsr info.' );
300        }
301        Assert::invariant( $tsr->end >= $tsr->start, 'Bad TSR' );
302        return $tsr->substr( ...$source );
303    }
304
305    /**
306     * Get a token from some PHP structure. Used by the PHPUnit tests.
307     *
308     * @param KV|Token|array|string|int|float|bool|null $input
309     * @return Token|string|int|float|bool|null|array<Token|string|int|float|bool|null>
310     */
311    public static function getToken( $input ) {
312        if ( !$input ) {
313            return $input;
314        }
315        $codec = new CompatJsonCodec();
316        return $codec->newFromJsonArray( $input, self::hint() );
317    }
318
319    public function fetchExpandedAttrValue( string $key ): ?DocumentFragment {
320        if ( preg_match(
321            '/mw:ExpandedAttrs/', $this->getAttributeV( 'typeof' ) ?? ''
322        ) ) {
323            $dmw = $this->dataMw;
324            if ( !isset( $dmw->attribs ) ) {
325                return null;
326            }
327            foreach ( $dmw->attribs as $attr ) {
328                if ( $attr->getKeyString() === $key ) {
329                    return $attr->value['html'] ?? null;
330                }
331            }
332        }
333        return null;
334    }
335
336}