Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 101
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
DataMw
0.00% covered (danger)
0.00%
0 / 101
0.00% covered (danger)
0.00%
0 / 10
1980
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 isEmpty
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getExtAttribs
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getExtAttrib
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setExtAttrib
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 __clone
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
156
 jsonClassHintFor
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 toJsonArray
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
110
 newFromJsonArray
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
72
 jsonClassCodec
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\Parsoid\NodeData;
5
6use Psr\Container\ContainerInterface;
7use stdClass;
8use Wikimedia\JsonCodec\Hint;
9use Wikimedia\JsonCodec\JsonClassCodec;
10use Wikimedia\JsonCodec\JsonCodecable;
11use Wikimedia\JsonCodec\JsonCodecInterface;
12use Wikimedia\Parsoid\DOM\DocumentFragment;
13use Wikimedia\Parsoid\Tokens\SourceRange;
14use Wikimedia\Parsoid\Tokens\Token;
15use Wikimedia\Parsoid\Utils\DOMDataUtils;
16
17/**
18 * Editing data for a DOM node.  Managed by DOMDataUtils::get/setDataMw().
19 *
20 * To reduce memory usage, most of the properties need to be dynamic, but
21 * we use the property declarations below to allow type checking.
22 *
23 * @property list<string|TemplateInfo> $parts
24 * @property string $name
25 * @property string $extPrefix
26 * @property string $extSuffix
27 * @property list<DataMwAttrib> $attribs Extended attributes of an HTML tag
28 * @property string $src
29 * @property DocumentFragment $caption
30 * @property string $thumb
31 * @property bool $autoGenerated
32 * @property list<DataMwError> $errors
33 * @property DataMwBody $body
34 * @property mixed $html
35 * @property float $scale
36 * @property string $starttime
37 * @property string $endtime
38 * @property string $thumbtime
39 * @property string $page
40 * == Annotations ==
41 * @property string $rangeId
42 * @property SourceRange $wtOffsets
43 * @property bool $extendedRange
44 * @property stdClass $attrs Attributes for an extension tag or annotation (T367616 should be renamed)
45 */
46#[\AllowDynamicProperties]
47class DataMw implements JsonCodecable {
48
49    public function __construct( array $initialVals = [] ) {
50        foreach ( $initialVals as $k => $v ) {
51            switch ( $k ) {
52                case 'attrs':
53                    // T367616: facilitate renaming.
54                    $this->attrs = $v;
55                    break;
56                // Add cases here for components which should be instantiated
57                // as proper classes.
58                default:
59                    $this->$k = $v;
60                    break;
61            }
62        }
63    }
64
65    /** Returns true iff there are no dynamic properties of this object. */
66    public function isEmpty(): bool {
67        $result = (array)$this;
68        return $result === [];
69    }
70
71    /**
72     * Helper method to facilitate renaming the 'attrs' property to
73     * 'extAttribs' (T367616).
74     * @note that numeric key values will be converted from string
75     *   to int by PHP when they are used as array keys
76     * @return ?array<string|int,string|array<Token|string>>
77     */
78    public function getExtAttribs(): ?array {
79        if ( isset( $this->attrs ) ) {
80            return (array)$this->attrs;
81        }
82        return null;
83    }
84
85    /**
86     * Helper method to facilitate renaming the 'attrs' property to
87     * 'extAttribs' (T367616).
88     * @param string $name
89     * @return string|array<Token|string>|null
90     */
91    public function getExtAttrib( string $name ) {
92        return $this->attrs->$name ?? null;
93    }
94
95    /**
96     * Helper method to facilitate renaming the 'attrs' property to
97     * 'extAttribs' (T367616).
98     * @param string $name
99     * @param string|array<Token|string>|null $value
100     *  Setting to null will unset it from the array.
101     */
102    public function setExtAttrib( string $name, $value ): void {
103        if ( !isset( $this->attrs ) ) {
104            $this->attrs = (object)[];
105        }
106        if ( $value === null ) {
107            unset( $this->attrs->$name );
108        } else {
109            $this->attrs->$name = $value;
110        }
111    }
112
113    public function __clone() {
114        // Deep clone non-primitive properties
115        if ( isset( $this->parts ) ) {
116            foreach ( $this->parts as &$part ) {
117                if ( !is_string( $part ) ) {
118                    $part = clone $part;
119                }
120            }
121        }
122        // Properties which are lists of cloneable objects
123        foreach ( [ 'attribs', 'errors' ] as $prop ) {
124            if ( isset( $this->$prop ) ) {
125                foreach ( $this->$prop as &$item ) {
126                    $item = clone $item;
127                }
128            }
129        }
130        // Properties which are cloneable objects
131        foreach ( [ 'body', 'wtOffsets' ] as $prop ) {
132            if ( isset( $this->$prop ) ) {
133                $this->$prop = clone $this->$prop;
134            }
135        }
136        // Document fragments are special
137        if ( isset( $this->caption ) ) {
138            $oldCaption = $this->caption;
139            $this->caption = $oldCaption->cloneNode( true );
140            DOMDataUtils::dedupeNodeData( $oldCaption, $this->caption );
141        }
142        // Generic stdClass, use PHP serialization as a kludge
143        foreach ( [ 'attrs', ] as $prop ) {
144            if ( isset( $this->$prop ) ) {
145                $this->$prop = unserialize( serialize( $this->$prop ) );
146            }
147        }
148    }
149
150    /** @inheritDoc */
151    public static function jsonClassHintFor( string $keyname ) {
152        static $hints = null;
153        if ( $hints === null ) {
154            $hints = [
155                'attribs' => Hint::build( DataMwAttrib::class, Hint::USE_SQUARE, Hint::LIST ),
156                // T367616: 'attrs' should be renamed to 'extAttribs' in
157                // a future revision of the MediaWiki DOM Spec
158                'attrs' => Hint::build( stdClass::class, Hint::ALLOW_OBJECT ),
159                'body' => Hint::build( DataMwBody::class, Hint::ALLOW_OBJECT ),
160                'wtOffsets' => Hint::build( SourceRange::class, Hint::USE_SQUARE ),
161                'parts' => Hint::build( TemplateInfo::class, Hint::STDCLASS, Hint::LIST ),
162                'errors' => Hint::build( DataMwError::class, Hint::LIST ),
163                // 'caption' is not hinted as DocumentFragment because we
164                // manually encode/decode it for MW Dom Spec 2.8.0 compat.
165            ];
166        }
167        return $hints[$keyname] ?? null;
168    }
169
170    /** @inheritDoc */
171    public function toJsonArray( JsonCodecInterface $codec ): array {
172        $result = (array)$this;
173        // T367141: Third party clients (eg Cite) create arrays instead of
174        // error objects.  We should convert them to proper DataMwError
175        // objects once those exist.
176        if ( isset( $result['errors'] ) ) {
177            $result['errors'] = array_map(
178                static fn ( $e ) => is_array( $e ) ? DataMwError::newFromJsonArray( $e ) :
179                    ( $e instanceof DataMwError ? $e : DataMwError::newFromJsonArray( (array)$e ) ),
180                $result['errors']
181            );
182        }
183        // Legacy encoding of parts.
184        if ( isset( $result['parts'] ) ) {
185            $result['parts'] = array_map( static function ( $p ) {
186                if ( $p instanceof TemplateInfo ) {
187                    $type = $p->type ?? 'template';
188                    if ( $type === 'parserfunction' ) {
189                        $type = 'template';
190                    } elseif ( $type === 'v3parserfunction' ) {
191                        $type = 'parserfunction';
192                    }
193                    $pp = (object)[];
194                    $pp->$type = $p;
195                    return $pp;
196                }
197                return $p;
198            }, $result['parts'] );
199        }
200        // Caption compatibility with MediaWiki DOM Spec 2.8.0
201        // See [[mw:Parsoid/MediaWiki DOM spec/Rich Attributes]] Phase 3
202        // for discussion about alternate _h/_t marking for DocumentFragments
203        if ( isset( $result['caption'] ) ) {
204            $c = $codec->toJsonArray( $result['caption'], DocumentFragment::class );
205            if ( is_string( $c['_h'] ?? null ) ) {
206                $result['caption'] = $c['_h'];
207            } else {
208                $result['caption'] = $c;
209            }
210        }
211        return $result;
212    }
213
214    /** @inheritDoc */
215    public static function newFromJsonArray( JsonCodecInterface $codec, array $json ): DataMw {
216        // Decode legacy encoding of parts.
217        if ( isset( $json['parts'] ) ) {
218            $json['parts'] = array_map( static function ( $p ) {
219                if ( is_object( $p ) ) {
220                    $ptype = $type = 'template';
221                    if ( isset( $p->templatearg ) ) {
222                        $ptype = $type = 'templatearg';
223                    } elseif ( isset( $p->parserfunction ) ) {
224                        $type = 'parserfunction';
225                        $ptype = 'v3parserfunction';
226                    }
227                    $p = $p->$type;
228                    if ( isset( $p->func ) ) {
229                        $ptype = 'parserfunction';
230                    }
231                    $p->type = $ptype;
232                }
233                return $p;
234            }, $json['parts'] );
235        }
236        // Usually '_h' or '_t' is used as a marker for captions, but
237        // allow a bare string as well.
238        $c = $json['caption'] ?? null;
239        $c = is_string( $c ) ? [ '_h' => $c ] : $c;
240        if ( $c !== null ) {
241            $json['caption'] =
242                $codec->newFromJsonArray( $c, DocumentFragment::class );
243        }
244        return new DataMw( $json );
245    }
246
247    /**
248     * Custom JsonClassCodec for DataMw.
249     *
250     * Because the 'caption' field has an embedded DocumentFragment that
251     * /doesn't/ use the standard encoding, we need to use a custom
252     * class codec which allows us to manually encode
253     * the DocumentFragment (by passing the codec itself to the
254     * serialization/deserialization methods).
255     */
256    public static function jsonClassCodec(
257        JsonCodecInterface $codec, ContainerInterface $serviceContainer
258    ): JsonClassCodec {
259        return new class( $codec ) implements JsonClassCodec {
260            private JsonCodecInterface $codec;
261
262            public function __construct( JsonCodecInterface $codec ) {
263                $this->codec = $codec;
264            }
265
266            /** @inheritDoc */
267            public function toJsonArray( $obj ): array {
268                return $obj->toJsonArray( $this->codec );
269            }
270
271            /** @inheritDoc */
272            public function newFromJsonArray( string $className, array $json ) {
273                return $className::newFromJsonArray( $this->codec, $json );
274            }
275
276            /** @inheritDoc */
277            public function jsonClassHintFor( string $className, string $keyName ) {
278                return $className::jsonClassHintFor( $keyName );
279            }
280        };
281    }
282}