Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
51.89% |
55 / 106 |
|
35.00% |
7 / 20 |
CRAP | |
0.00% |
0 / 1 |
WikitextPFragment | |
51.89% |
55 / 106 |
|
35.00% |
7 / 20 |
304.61 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
newFromWt | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
newFromSplitWt | |
65.00% |
26 / 40 |
|
0.00% |
0 / 1 |
31.89 | |||
newFromLiteral | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
castFromPFragment | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
isEmpty | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isAtomic | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
asDom | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
asMarkedWikitext | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
startsWithMarker | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
endsWithMarker | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
containsMarker | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
split | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
killMarkers | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
markerSkipCallback | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
trim | |
64.71% |
11 / 17 |
|
0.00% |
0 / 1 |
4.70 | |||
concat | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
toJsonArray | |
60.00% |
6 / 10 |
|
0.00% |
0 / 1 |
2.26 | |||
newFromJsonArray | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
jsonClassHintFor | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace Wikimedia\Parsoid\Fragments; |
5 | |
6 | use Wikimedia\JsonCodec\Hint; |
7 | use Wikimedia\Parsoid\Core\DomSourceRange; |
8 | use Wikimedia\Parsoid\DOM\DocumentFragment; |
9 | use Wikimedia\Parsoid\Ext\ParsoidExtensionAPI; |
10 | use Wikimedia\Parsoid\Utils\Utils; |
11 | |
12 | /** |
13 | * A non-atomic fragment comprised of a wikitext string and a strip state. |
14 | * |
15 | * The wikitext string may contain strip markers, which are |
16 | * placeholders corresponding to other atomic fragments. The internal |
17 | * StripState holds the mapping between placeholders and the |
18 | * corresponding atomic fragments. |
19 | * |
20 | * WikitextPFragments are not atomic, and so a WikitextPFragment |
21 | * should never contain strip markers corresponding to other |
22 | * WikitextPFragments. |
23 | */ |
24 | class WikitextPFragment extends PFragment { |
25 | |
26 | public const TYPE_HINT = 'wt'; |
27 | |
28 | /** Wikitext value of this fragment, with embedded strip markers. */ |
29 | private string $value; |
30 | |
31 | /** The strip state giving the value of the embedded strip markers. */ |
32 | private ?StripState $stripState; |
33 | |
34 | private function __construct( |
35 | string $value, ?DomSourceRange $srcOffsets, ?StripState $stripState |
36 | ) { |
37 | parent::__construct( $srcOffsets ); |
38 | $this->value = $value; |
39 | $this->stripState = $stripState; |
40 | } |
41 | |
42 | /** |
43 | * Return a new WikitextPFragment consisting of the given fragment |
44 | * of wikitext, and an optional source string for it. |
45 | */ |
46 | public static function newFromWt( string $wikitext, ?DomSourceRange $srcOffsets ): WikitextPFragment { |
47 | return new self( $wikitext, $srcOffsets, null ); |
48 | } |
49 | |
50 | /** |
51 | * Return a new WikitextPFragment consisting of the given array of |
52 | * pieces concatenated together. Each piece can contain either |
53 | * a `string` of wikitext (without strip markers) or a PFragment. |
54 | * @param array<string|PFragment> $pieces |
55 | */ |
56 | public static function newFromSplitWt( array $pieces, ?DomSourceRange $srcOffsets = null ): WikitextPFragment { |
57 | $wikitext = []; |
58 | $isFirst = true; |
59 | $lastIsMarker = false; |
60 | $firstDSR = null; |
61 | $lastDSR = null; |
62 | $ss = StripState::new(); |
63 | foreach ( $pieces as $p ) { |
64 | if ( $p instanceof PFragment && !$p->isAtomic() ) { |
65 | // Don't create a strip marker for non-atomic fragments, |
66 | // instead concatenate their wikitext components |
67 | $p = self::castFromPFragment( $p ); |
68 | } |
69 | if ( $p === '' ) { |
70 | continue; |
71 | } elseif ( $p instanceof PFragment && $p->isEmpty() ) { |
72 | continue; |
73 | } elseif ( $p instanceof WikitextPFragment ) { |
74 | // XXX we could also avoid adding the <nowiki> if we notice |
75 | // that our source ranges are adjacent (ie, the wikitext |
76 | // strings were adjacent in the source document) |
77 | if ( !( $isFirst || $lastIsMarker || $p->startsWithMarker() ) ) { |
78 | $wikitext[] = '<nowiki/>'; |
79 | } |
80 | $wikitext[] = $p->value; |
81 | if ( $p->stripState !== null ) { |
82 | $ss->addAllFrom( $p->stripState ); |
83 | } |
84 | if ( $isFirst ) { |
85 | $firstDSR = $p->getSrcOffsets(); |
86 | } |
87 | $lastIsMarker = $p->endsWithMarker(); |
88 | $lastDSR = $p->getSrcOffsets(); |
89 | } elseif ( !is_string( $p ) ) { |
90 | // This is an atomic PFragment |
91 | $wikitext[] = $ss->addWtItem( $p ); |
92 | if ( $isFirst ) { |
93 | $firstDSR = $p->getSrcOffsets(); |
94 | } |
95 | $lastIsMarker = true; |
96 | $lastDSR = $p->getSrcOffsets(); |
97 | } else { |
98 | // This is a wikitext string |
99 | if ( !( $isFirst || $lastIsMarker ) ) { |
100 | $wikitext[] = '<nowiki/>'; |
101 | } |
102 | $wikitext[] = $p; |
103 | $lastIsMarker = false; |
104 | $lastDSR = null; |
105 | } |
106 | $isFirst = false; |
107 | } |
108 | return new self( |
109 | implode( '', $wikitext ), |
110 | // Create DSR if first and last pieces were fragments. |
111 | $srcOffsets ?? self::joinSourceRange( $firstDSR, $lastDSR ), |
112 | $ss->isEmpty() ? null : $ss |
113 | ); |
114 | } |
115 | |
116 | /** |
117 | * Returns a new WikitextPFragment from the given literal string |
118 | * and optional source offsets. |
119 | * |
120 | * Unlike LiteralStringPFragment, the resulting fragment is |
121 | * non-atomic -- it will not be an opaque strip marked but instead |
122 | * will consists of escaped wikitext that will evaluate to the |
123 | * desired string value. |
124 | * |
125 | * @see LiteralStringPFragment::newFromLiteral() for an atomic |
126 | * fragment equivalent. |
127 | * |
128 | * @param string $value The literal string |
129 | * @param ?DomSourceRange $srcOffsets The source range corresponding to |
130 | * this literal string, if there is one |
131 | */ |
132 | public static function newFromLiteral( string $value, ?DomSourceRange $srcOffsets ): WikitextPFragment { |
133 | return self::newFromWt( Utils::escapeWt( $value ), $srcOffsets ); |
134 | } |
135 | |
136 | /** |
137 | * Return a WikitextPFragment corresponding to the given PFragment. |
138 | * If the fragment is not already a WikitextPFragment, this will convert |
139 | * it using PFragment::asMarkedWikitext(). |
140 | */ |
141 | public static function castFromPFragment( PFragment $fragment ): WikitextPFragment { |
142 | if ( $fragment instanceof WikitextPFragment ) { |
143 | return $fragment; |
144 | } |
145 | $ss = StripState::new(); |
146 | $wikitext = $fragment->asMarkedWikitext( $ss ); |
147 | return new self( |
148 | $wikitext, $fragment->srcOffsets, $ss->isEmpty() ? null : $ss |
149 | ); |
150 | } |
151 | |
152 | /** @inheritDoc */ |
153 | public function isEmpty(): bool { |
154 | return $this->value === ''; |
155 | } |
156 | |
157 | /** @return false */ |
158 | public function isAtomic(): bool { |
159 | return false; |
160 | } |
161 | |
162 | /** @inheritDoc */ |
163 | public function asDom( ParsoidExtensionAPI $ext, bool $release = false ): DocumentFragment { |
164 | return $ext->wikitextToDOM( $this, [], true ); |
165 | } |
166 | |
167 | /** @inheritDoc */ |
168 | public function asMarkedWikitext( StripState $stripState ): string { |
169 | if ( $this->stripState !== null ) { |
170 | $stripState->addAllFrom( $this->stripState ); |
171 | } |
172 | return $this->value; |
173 | } |
174 | |
175 | private function startsWithMarker(): bool { |
176 | return StripState::startsWithStripMarker( $this->value ); |
177 | } |
178 | |
179 | private function endsWithMarker(): bool { |
180 | return StripState::endsWithStripMarker( $this->value ); |
181 | } |
182 | |
183 | /** |
184 | * Returns true if this fragment contains some non-wikitext content. |
185 | */ |
186 | public function containsMarker(): bool { |
187 | return StripState::containsStripMarker( $this->value ); |
188 | } |
189 | |
190 | /** |
191 | * Split this fragment at its strip markers and return an array |
192 | * which alternates between string items and PFragment items. |
193 | * The first and last items are guaranteed to be strings, and the |
194 | * array length is guaranteed to be odd and at least 1. |
195 | * @return list<string|PFragment> |
196 | */ |
197 | public function split(): array { |
198 | if ( $this->stripState === null ) { |
199 | return [ $this->value ]; |
200 | } |
201 | return $this->stripState->splitWt( $this->value ); |
202 | } |
203 | |
204 | /** |
205 | * Return a version of this wikitext fragment with all strip markers |
206 | * removed. |
207 | * @return string |
208 | */ |
209 | public function killMarkers(): string { |
210 | return implode( array_filter( $this->split(), "is_string" ) ); |
211 | } |
212 | |
213 | /** @inheritDoc */ |
214 | public function markerSkipCallback( callable $callback ): PFragment { |
215 | return PFragment::fromSplitWt( |
216 | array_map( |
217 | static fn ( $el ) => is_string( $el ) ? $callback( $el ) : $el, |
218 | $this->split() |
219 | ), $this->srcOffsets |
220 | ); |
221 | } |
222 | |
223 | /** |
224 | * Trim leading and trailing whitespace from this fragment. |
225 | * |
226 | * If the result is just a strip marker, will return the fragment |
227 | * corresponding to that strip marker; that is, this method is |
228 | * not guaranteed to return a WikitextPFragment. |
229 | * |
230 | * @return PFragment |
231 | */ |
232 | public function trim(): PFragment { |
233 | $pieces = $this->split(); |
234 | |
235 | $oldSize = strlen( $pieces[0] ); |
236 | $pieces[0] = ltrim( $pieces[0] ); |
237 | $startTrim = $oldSize - strlen( $pieces[0] ); |
238 | |
239 | $end = count( $pieces ) - 1; |
240 | $oldSize = strlen( $pieces[$end] ); |
241 | $pieces[$end] = rtrim( $pieces[$end] ); |
242 | $endTrim = $oldSize - strlen( $pieces[$end] ); |
243 | |
244 | $newDsr = null; |
245 | if ( $this->srcOffsets !== null ) { |
246 | [ $start, $end ] = [ $this->srcOffsets->start, $this->srcOffsets->end ]; |
247 | if ( $start !== null ) { |
248 | $start += $startTrim; |
249 | } |
250 | if ( $end !== null ) { |
251 | $end -= $endTrim; |
252 | } |
253 | $newDsr = new DomSourceRange( $start, $end, null, null ); |
254 | } |
255 | return PFragment::fromSplitWt( $pieces, $newDsr ); |
256 | } |
257 | |
258 | /** |
259 | * Return a WikitextPFragment representing the concatenation of |
260 | * the given fragments, as wikitext. |
261 | */ |
262 | public static function concat( PFragment ...$fragments ): self { |
263 | return self::newFromSplitWt( $fragments ); |
264 | } |
265 | |
266 | // JsonCodecable implementation |
267 | |
268 | /** @inheritDoc */ |
269 | public function toJsonArray(): array { |
270 | $pieces = $this->split(); |
271 | if ( count( $pieces ) === 1 ) { |
272 | $wt = $pieces[0]; |
273 | $ret = [ |
274 | self::TYPE_HINT => $wt, |
275 | ]; |
276 | } else { |
277 | $ret = [ |
278 | self::TYPE_HINT => $pieces, |
279 | ]; |
280 | } |
281 | return $ret + parent::toJsonArray(); |
282 | } |
283 | |
284 | /** @inheritDoc */ |
285 | public static function newFromJsonArray( array $json ): self { |
286 | $v = $json[self::TYPE_HINT]; |
287 | if ( is_string( $v ) ) { |
288 | $v = [ $v ]; |
289 | } |
290 | return self::newFromSplitWt( $v, $json['dsr'] ?? null ); |
291 | } |
292 | |
293 | /** @inheritDoc */ |
294 | public static function jsonClassHintFor( string $keyName ) { |
295 | if ( $keyName === self::TYPE_HINT ) { |
296 | return Hint::build( PFragment::class, Hint::INHERITED, Hint::LIST ); |
297 | } |
298 | return parent::jsonClassHintFor( $keyName ); |
299 | } |
300 | } |