Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 74 |
|
0.00% |
0 / 9 |
CRAP | |
0.00% |
0 / 1 |
DOMDataCodec | |
0.00% |
0 / 74 |
|
0.00% |
0 / 9 |
506 | |
0.00% |
0 / 1 |
setOptions | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
makeTID | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
splitTID | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
ensureFragmentIndex | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
setUniqueTID | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
popTID | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
flatten | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 | |||
defaultValue | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 | |||
__construct | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace Wikimedia\Parsoid\Utils; |
5 | |
6 | use Wikimedia\JsonCodec\JsonClassCodec; |
7 | use Wikimedia\JsonCodec\JsonCodec; |
8 | use Wikimedia\Parsoid\DOM\Document; |
9 | use Wikimedia\Parsoid\DOM\DocumentFragment; |
10 | use Wikimedia\Parsoid\DOM\Element; |
11 | |
12 | /** |
13 | * Customized subclass of JsonCodec for serialization of rich attributes. |
14 | */ |
15 | class 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 | } |