Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 74
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
DOMDataCodec
0.00% covered (danger)
0.00%
0 / 74
0.00% covered (danger)
0.00%
0 / 9
506
0.00% covered (danger)
0.00%
0 / 1
 setOptions
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 makeTID
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 splitTID
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 ensureFragmentIndex
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 setUniqueTID
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 popTID
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 flatten
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 defaultValue
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 __construct
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\Parsoid\Utils;
5
6use Wikimedia\JsonCodec\JsonClassCodec;
7use Wikimedia\JsonCodec\JsonCodec;
8use Wikimedia\Parsoid\DOM\Document;
9use Wikimedia\Parsoid\DOM\DocumentFragment;
10use Wikimedia\Parsoid\DOM\Element;
11
12/**
13 * Customized subclass of JsonCodec for serialization of rich attributes.
14 */
15class DOMDataCodec extends JsonCodec {
16    public Document $ownerDoc;
17    public array $options = [];
18    private ?array $fragmentIndex = null;
19
20    public function setOptions( array $options ): array {
21        $oldOptions = $this->options;
22        $this->options = $options;
23        return $oldOptions;
24    }
25
26    private static function makeTID( string $base, int $count ): string {
27        return $count === 0 ? $base : "$base-$count";
28    }
29
30    private static function splitTID( string $tid ): array {
31        [ $base, $count ] = array_pad( explode( '-', $tid, 2 ), 2, '0' );
32        return [ $base, intval( $count ) ];
33    }
34
35    private function ensureFragmentIndex(): void {
36        if ( $this->fragmentIndex !== null ) {
37            return;
38        }
39        $doc = $this->ownerDoc;
40        $this->fragmentIndex = [];
41        $templates = DOMCompat::querySelectorAll(
42            $doc, 'head > template[data-tid]'
43        );
44        foreach ( $templates as $t ) {
45            $tid = DOMCompat::getAttribute( $t, 'data-tid' );
46            [ $base, $count ] = self::splitTID( $tid );
47            $this->fragmentIndex[$base][$count] = $t;
48        }
49    }
50
51    public function setUniqueTID( string $tidBase, Element $template ): string {
52        $this->ensureFragmentIndex();
53        $max = $this->fragmentIndex[$tidBase]['max'] ?? null;
54        if ( $max === null ) {
55            $max = max( array_keys( $this->fragmentIndex[$tidBase] ?? [] ) ?: [ -1 ] );
56        }
57        $count = ++$max;
58        // storing max here ensures we're not O(N^2)
59        $this->fragmentIndex[$tidBase]['max'] = $count;
60        $this->fragmentIndex[$tidBase][$count] = $template;
61        $tid = self::makeTID( $tidBase, $count );
62        $template->setAttribute( 'data-tid', $tid );
63        return $tid;
64    }
65
66    public function popTID( string $tid ): Element {
67        [ $base, $count ] = self::splitTID( $tid );
68        $this->ensureFragmentIndex();
69        $t = $this->fragmentIndex[$base][$count];
70        $t->parentNode->removeChild( $t );
71        unset( $this->fragmentIndex[$base][$count] );
72        // reset 'max' because it's not guaranteed to be correct any more
73        unset( $this->fragmentIndex[$base]['max'] );
74        return $t;
75    }
76
77    /**
78     * Return a flattened string representation of this complex object.
79     * @param object $obj
80     * @return ?string
81     */
82    public function flatten( $obj ): ?string {
83        $codec = $this->codecFor( get_class( $obj ) );
84        if ( $codec !== null && method_exists( $codec, 'flatten' ) ) {
85            // @phan-suppress-next-line PhanUndeclaredMethod
86            return $codec->flatten( $obj );
87        }
88        if ( method_exists( $obj, 'flatten' ) ) {
89            return $obj->flatten();
90        }
91        return null;
92    }
93
94    /**
95     * Return an appropriate default value for objects of the given type.
96     * @phan-template T
97     * @param class-string<T> $className
98     * @return T
99     */
100    public function defaultValue( $className ): ?object {
101        $codec = $this->codecFor( $className );
102        if ( $codec !== null && method_exists( $codec, 'defaultValue' ) ) {
103            // @phan-suppress-next-line PhanUndeclaredMethod
104            return $codec->defaultValue();
105        }
106        if ( method_exists( $className, 'defaultValue' ) ) {
107            return $className::defaultValue();
108        }
109        return null;
110    }
111
112    /**
113     * Create a new DOMDataCodec.
114     * @param Document $ownerDoc
115     * @param array $options
116     */
117    public function __construct( Document $ownerDoc, array $options ) {
118        parent::__construct();
119        $this->ownerDoc = $ownerDoc;
120        $this->options = $options;
121        // Add codec for DocumentFragment
122        $this->addCodecFor( DocumentFragment::class, new class( $this ) implements JsonClassCodec {
123            private DOMDataCodec $codec;
124
125            public function __construct( DOMDataCodec $codec ) {
126                $this->codec = $codec;
127            }
128
129            /**
130             * Flatten the given DocumentFragment into a string.
131             * @param DocumentFragment $df
132             * @return string
133             */
134            public function flatten( DocumentFragment $df ): string {
135                '@phan-var DocumentFragment $df';
136                return $df->textContent;
137            }
138
139            /**
140             * Return a default value for a new attribute of this type.
141             * @return DocumentFragment
142             */
143            public function defaultValue(): DocumentFragment {
144                return $this->codec->ownerDoc->createDocumentFragment();
145            }
146
147            /** @inheritDoc */
148            public function toJsonArray( $df ): array {
149                '@phan-var DocumentFragment $df';
150                // Store rich attributes in the document fragment
151                // before serializing it; this should share this codec
152                // and so the fragment bank numbering won't conflict.
153                DOMDataUtils::visitAndStoreDataAttribs(
154                    $df, $this->codec->options
155                );
156                if ( $this->codec->options['useFragmentBank'] ?? false ) {
157                    $t = $this->codec->ownerDoc->createElement( 'template' );
158                    DOMUtils::migrateChildrenBetweenDocs( $df, DOMCompat::getTemplateElementContent( $t ) );
159                    DOMCompat::getHead( $this->codec->ownerDoc )->appendChild( $t );
160                    // Assign a unique ID.
161                    // Start with a content hash based on text contents; we
162                    // could do better than this if we needed to, but the basic
163                    // goal is to make the IDs relatively stable to avoid
164                    // unnecessary character diffs.
165                    $hash = hash( 'sha256', DOMCompat::getTemplateElementContent( $t )->textContent, true );
166                    // Base64 is 6 bits per char, so 6 bytes ought to be 8
167                    // characters with no padding
168                    $tidBase = base64_encode( substr( $hash, 0, 6 ) );
169                    $tid = $this->codec->setUniqueTID( $tidBase, $t );
170                    return [ '_t' => $tid ];
171                } else {
172                    return [ '_h' => DOMUtils::getFragmentInnerHTML( $df ) ];
173                }
174            }
175
176            /** @inheritDoc */
177            public function newFromJsonArray( string $className, array $json ) {
178                $df = $this->codec->ownerDoc->createDocumentFragment();
179                if ( isset( $json['_t'] ) ) {
180                    // fragment bank representation
181                    $t = $this->codec->popTID( $json['_t'] );
182                    DOMUtils::migrateChildrenBetweenDocs( DOMCompat::getTemplateElementContent( $t ), $df );
183                } else {
184                    DOMUtils::setFragmentInnerHTML( $df, $json['_h'] );
185                }
186                DOMDataUtils::visitAndLoadDataAttribs( $df, $this->codec->options );
187                return $df; // @phan-suppress-current-line PhanTypeMismatchReturn
188            }
189
190            /** @inheritDoc */
191            public function jsonClassHintFor( string $className, string $keyName ): ?string {
192                return null;
193            }
194        } );
195    }
196}