Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
54.08% |
53 / 98 |
|
29.41% |
5 / 17 |
CRAP | |
0.00% |
0 / 1 |
WikitextPFragment | |
54.08% |
53 / 98 |
|
29.41% |
5 / 17 |
231.44 | |
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 | |||
split | |
0.00% |
0 / 3 |
|
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 | * Split this fragment at its strip markers and return an array |
185 | * which alternates between string items and PFragment items. |
186 | * The first and last items are guaranteed to be strings, and the |
187 | * array length is guaranteed to be odd and at least 1. |
188 | * @return list<string|PFragment> |
189 | */ |
190 | public function split(): array { |
191 | if ( $this->stripState === null ) { |
192 | return [ $this->value ]; |
193 | } |
194 | return $this->stripState->splitWt( $this->value ); |
195 | } |
196 | |
197 | /** |
198 | * Trim leading and trailing whitespace from this fragment. |
199 | * |
200 | * If the result is just a strip marker, will return the fragment |
201 | * corresponding to that strip marker; that is, this method is |
202 | * not guaranteed to return a WikitextPFragment. |
203 | * |
204 | * @return PFragment |
205 | */ |
206 | public function trim(): PFragment { |
207 | $pieces = $this->split(); |
208 | |
209 | $oldSize = strlen( $pieces[0] ); |
210 | $pieces[0] = ltrim( $pieces[0] ); |
211 | $startTrim = $oldSize - strlen( $pieces[0] ); |
212 | |
213 | $end = count( $pieces ) - 1; |
214 | $oldSize = strlen( $pieces[$end] ); |
215 | $pieces[$end] = rtrim( $pieces[$end] ); |
216 | $endTrim = $oldSize - strlen( $pieces[$end] ); |
217 | |
218 | $newDsr = null; |
219 | if ( $this->srcOffsets !== null ) { |
220 | [ $start, $end ] = [ $this->srcOffsets->start, $this->srcOffsets->end ]; |
221 | if ( $start !== null ) { |
222 | $start += $startTrim; |
223 | } |
224 | if ( $end !== null ) { |
225 | $end -= $endTrim; |
226 | } |
227 | $newDsr = new DomSourceRange( $start, $end, null, null ); |
228 | } |
229 | return PFragment::fromSplitWt( $pieces, $newDsr ); |
230 | } |
231 | |
232 | /** |
233 | * Return a WikitextPFragment representing the concatenation of |
234 | * the given fragments, as wikitext. |
235 | */ |
236 | public static function concat( PFragment ...$fragments ): self { |
237 | return self::newFromSplitWt( $fragments ); |
238 | } |
239 | |
240 | // JsonCodecable implementation |
241 | |
242 | /** @inheritDoc */ |
243 | public function toJsonArray(): array { |
244 | $pieces = $this->split(); |
245 | if ( count( $pieces ) === 1 ) { |
246 | $wt = $pieces[0]; |
247 | $ret = [ |
248 | self::TYPE_HINT => $wt, |
249 | ]; |
250 | } else { |
251 | $ret = [ |
252 | self::TYPE_HINT => $pieces, |
253 | ]; |
254 | } |
255 | return $ret + parent::toJsonArray(); |
256 | } |
257 | |
258 | /** @inheritDoc */ |
259 | public static function newFromJsonArray( array $json ): self { |
260 | $v = $json[self::TYPE_HINT]; |
261 | if ( is_string( $v ) ) { |
262 | $v = [ $v ]; |
263 | } |
264 | return self::newFromSplitWt( $v, $json['dsr'] ?? null ); |
265 | } |
266 | |
267 | /** @inheritDoc */ |
268 | public static function jsonClassHintFor( string $keyName ) { |
269 | if ( $keyName === self::TYPE_HINT ) { |
270 | return Hint::build( PFragment::class, Hint::INHERITED, Hint::LIST ); |
271 | } |
272 | return parent::jsonClassHintFor( $keyName ); |
273 | } |
274 | } |