Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
50.93% |
55 / 108 |
|
35.00% |
7 / 20 |
CRAP | |
0.00% |
0 / 1 |
| WikitextPFragment | |
50.93% |
55 / 108 |
|
35.00% |
7 / 20 |
345.46 | |
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 | |
61.90% |
26 / 42 |
|
0.00% |
0 / 1 |
42.11 | |||
| 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 | * @param ?DomSourceRange $srcOffsets |
| 56 | * @param bool $unsafeConcat False by default; when true suppresses |
| 57 | * the addition of <nowiki/> markers used to ensure adjacent wikitext |
| 58 | * strings do not interfere with each other. |
| 59 | */ |
| 60 | public static function newFromSplitWt( |
| 61 | array $pieces, |
| 62 | ?DomSourceRange $srcOffsets = null, |
| 63 | bool $unsafeConcat = false |
| 64 | ): WikitextPFragment { |
| 65 | $wikitext = []; |
| 66 | $isFirst = true; |
| 67 | $lastIsMarker = false; |
| 68 | $firstDSR = null; |
| 69 | $lastDSR = null; |
| 70 | $ss = StripState::new(); |
| 71 | foreach ( $pieces as $p ) { |
| 72 | if ( $p instanceof PFragment && !$p->isAtomic() ) { |
| 73 | // Don't create a strip marker for non-atomic fragments, |
| 74 | // instead concatenate their wikitext components |
| 75 | $p = self::castFromPFragment( $p ); |
| 76 | } |
| 77 | if ( $p === '' ) { |
| 78 | continue; |
| 79 | } elseif ( $p instanceof PFragment && $p->isEmpty() ) { |
| 80 | continue; |
| 81 | } elseif ( $p instanceof WikitextPFragment ) { |
| 82 | // XXX we could also avoid adding the <nowiki> if we notice |
| 83 | // that our source ranges are adjacent (ie, the wikitext |
| 84 | // strings were adjacent in the source document) |
| 85 | if ( !( $isFirst || $lastIsMarker || $p->startsWithMarker() ) ) { |
| 86 | if ( !$unsafeConcat ) { |
| 87 | $wikitext[] = '<nowiki/>'; |
| 88 | } |
| 89 | } |
| 90 | $wikitext[] = $p->value; |
| 91 | if ( $p->stripState !== null ) { |
| 92 | $ss->addAllFrom( $p->stripState ); |
| 93 | } |
| 94 | if ( $isFirst ) { |
| 95 | $firstDSR = $p->getSrcOffsets(); |
| 96 | } |
| 97 | $lastIsMarker = $p->endsWithMarker(); |
| 98 | $lastDSR = $p->getSrcOffsets(); |
| 99 | } elseif ( !is_string( $p ) ) { |
| 100 | // This is an atomic PFragment |
| 101 | $wikitext[] = $ss->addWtItem( $p ); |
| 102 | if ( $isFirst ) { |
| 103 | $firstDSR = $p->getSrcOffsets(); |
| 104 | } |
| 105 | $lastIsMarker = true; |
| 106 | $lastDSR = $p->getSrcOffsets(); |
| 107 | } else { |
| 108 | // This is a wikitext string |
| 109 | if ( !( $isFirst || $lastIsMarker ) ) { |
| 110 | if ( !$unsafeConcat ) { |
| 111 | $wikitext[] = '<nowiki/>'; |
| 112 | } |
| 113 | } |
| 114 | $wikitext[] = $p; |
| 115 | $lastIsMarker = false; |
| 116 | $lastDSR = null; |
| 117 | } |
| 118 | $isFirst = false; |
| 119 | } |
| 120 | return new self( |
| 121 | implode( '', $wikitext ), |
| 122 | // Create DSR if first and last pieces were fragments. |
| 123 | $srcOffsets ?? self::joinSourceRange( $firstDSR, $lastDSR ), |
| 124 | $ss->isEmpty() ? null : $ss |
| 125 | ); |
| 126 | } |
| 127 | |
| 128 | /** |
| 129 | * Returns a new WikitextPFragment from the given literal string |
| 130 | * and optional source offsets. |
| 131 | * |
| 132 | * Unlike LiteralStringPFragment, the resulting fragment is |
| 133 | * non-atomic -- it will not be an opaque strip marker but instead |
| 134 | * will consists of escaped wikitext that will evaluate to the |
| 135 | * desired string value. |
| 136 | * |
| 137 | * @see LiteralStringPFragment::newFromLiteral() for an atomic |
| 138 | * fragment equivalent. |
| 139 | * |
| 140 | * @param string $value The literal string |
| 141 | * @param ?DomSourceRange $srcOffsets The source range corresponding to |
| 142 | * this literal string, if there is one |
| 143 | */ |
| 144 | public static function newFromLiteral( string $value, ?DomSourceRange $srcOffsets ): WikitextPFragment { |
| 145 | return self::newFromWt( Utils::escapeWt( $value ), $srcOffsets ); |
| 146 | } |
| 147 | |
| 148 | /** |
| 149 | * Return a WikitextPFragment corresponding to the given PFragment. |
| 150 | * If the fragment is not already a WikitextPFragment, this will convert |
| 151 | * it using PFragment::asMarkedWikitext(). |
| 152 | */ |
| 153 | public static function castFromPFragment( PFragment $fragment ): WikitextPFragment { |
| 154 | if ( $fragment instanceof WikitextPFragment ) { |
| 155 | return $fragment; |
| 156 | } |
| 157 | $ss = StripState::new(); |
| 158 | $wikitext = $fragment->asMarkedWikitext( $ss ); |
| 159 | return new self( |
| 160 | $wikitext, $fragment->srcOffsets, $ss->isEmpty() ? null : $ss |
| 161 | ); |
| 162 | } |
| 163 | |
| 164 | /** @inheritDoc */ |
| 165 | public function isEmpty(): bool { |
| 166 | return $this->value === ''; |
| 167 | } |
| 168 | |
| 169 | /** @return false */ |
| 170 | public function isAtomic(): bool { |
| 171 | return false; |
| 172 | } |
| 173 | |
| 174 | /** @inheritDoc */ |
| 175 | public function asDom( ParsoidExtensionAPI $extApi, bool $release = false ): DocumentFragment { |
| 176 | return $extApi->wikitextToDOM( $this, [], true ); |
| 177 | } |
| 178 | |
| 179 | /** @inheritDoc */ |
| 180 | public function asMarkedWikitext( StripState $stripState ): string { |
| 181 | if ( $this->stripState !== null ) { |
| 182 | $stripState->addAllFrom( $this->stripState ); |
| 183 | } |
| 184 | return $this->value; |
| 185 | } |
| 186 | |
| 187 | private function startsWithMarker(): bool { |
| 188 | return StripState::startsWithStripMarker( $this->value ); |
| 189 | } |
| 190 | |
| 191 | private function endsWithMarker(): bool { |
| 192 | return StripState::endsWithStripMarker( $this->value ); |
| 193 | } |
| 194 | |
| 195 | /** |
| 196 | * Returns true if this fragment contains some non-wikitext content. |
| 197 | */ |
| 198 | public function containsMarker(): bool { |
| 199 | return StripState::containsStripMarker( $this->value ); |
| 200 | } |
| 201 | |
| 202 | /** |
| 203 | * Split this fragment at its strip markers and return an array |
| 204 | * which alternates between string items and PFragment items. |
| 205 | * The first and last items are guaranteed to be strings, and the |
| 206 | * array length is guaranteed to be odd and at least 1. |
| 207 | * @return list<string|PFragment> |
| 208 | */ |
| 209 | public function split(): array { |
| 210 | if ( $this->stripState === null ) { |
| 211 | return [ $this->value ]; |
| 212 | } |
| 213 | return $this->stripState->splitWt( $this->value ); |
| 214 | } |
| 215 | |
| 216 | /** |
| 217 | * Return a version of this wikitext fragment with all strip markers |
| 218 | * removed. |
| 219 | * @return string |
| 220 | */ |
| 221 | public function killMarkers(): string { |
| 222 | return implode( array_filter( $this->split(), "is_string" ) ); |
| 223 | } |
| 224 | |
| 225 | /** @inheritDoc */ |
| 226 | public function markerSkipCallback( callable $callback ): PFragment { |
| 227 | return PFragment::fromSplitWt( |
| 228 | array_map( |
| 229 | static fn ( $el ) => is_string( $el ) ? $callback( $el ) : $el, |
| 230 | $this->split() |
| 231 | ), $this->srcOffsets |
| 232 | ); |
| 233 | } |
| 234 | |
| 235 | /** |
| 236 | * Trim leading and trailing whitespace from this fragment. |
| 237 | * |
| 238 | * If the result is just a strip marker, will return the fragment |
| 239 | * corresponding to that strip marker; that is, this method is |
| 240 | * not guaranteed to return a WikitextPFragment. |
| 241 | * |
| 242 | * @return PFragment |
| 243 | */ |
| 244 | public function trim(): PFragment { |
| 245 | $pieces = $this->split(); |
| 246 | |
| 247 | $oldSize = strlen( $pieces[0] ); |
| 248 | $pieces[0] = ltrim( $pieces[0] ); |
| 249 | $startTrim = $oldSize - strlen( $pieces[0] ); |
| 250 | |
| 251 | $end = count( $pieces ) - 1; |
| 252 | $oldSize = strlen( $pieces[$end] ); |
| 253 | $pieces[$end] = rtrim( $pieces[$end] ); |
| 254 | $endTrim = $oldSize - strlen( $pieces[$end] ); |
| 255 | |
| 256 | $newDsr = null; |
| 257 | if ( $this->srcOffsets !== null ) { |
| 258 | [ $start, $end ] = [ $this->srcOffsets->start, $this->srcOffsets->end ]; |
| 259 | if ( $start !== null ) { |
| 260 | $start += $startTrim; |
| 261 | } |
| 262 | if ( $end !== null ) { |
| 263 | $end -= $endTrim; |
| 264 | } |
| 265 | $newDsr = new DomSourceRange( $start, $end, null, null ); |
| 266 | } |
| 267 | return PFragment::fromSplitWt( $pieces, $newDsr ); |
| 268 | } |
| 269 | |
| 270 | /** |
| 271 | * Return a WikitextPFragment representing the concatenation of |
| 272 | * the given fragments, as wikitext. |
| 273 | */ |
| 274 | public static function concat( PFragment ...$fragments ): self { |
| 275 | return self::newFromSplitWt( $fragments ); |
| 276 | } |
| 277 | |
| 278 | // JsonCodecable implementation |
| 279 | |
| 280 | /** @inheritDoc */ |
| 281 | public function toJsonArray(): array { |
| 282 | $pieces = $this->split(); |
| 283 | if ( count( $pieces ) === 1 ) { |
| 284 | $wt = $pieces[0]; |
| 285 | $ret = [ |
| 286 | self::TYPE_HINT => $wt, |
| 287 | ]; |
| 288 | } else { |
| 289 | $ret = [ |
| 290 | self::TYPE_HINT => $pieces, |
| 291 | ]; |
| 292 | } |
| 293 | return $ret + parent::toJsonArray(); |
| 294 | } |
| 295 | |
| 296 | /** @inheritDoc */ |
| 297 | public static function newFromJsonArray( array $json ): self { |
| 298 | $v = $json[self::TYPE_HINT]; |
| 299 | if ( is_string( $v ) ) { |
| 300 | $v = [ $v ]; |
| 301 | } |
| 302 | return self::newFromSplitWt( $v, $json['dsr'] ?? null ); |
| 303 | } |
| 304 | |
| 305 | /** @inheritDoc */ |
| 306 | public static function jsonClassHintFor( string $keyName ) { |
| 307 | if ( $keyName === self::TYPE_HINT ) { |
| 308 | return Hint::build( PFragment::class, Hint::INHERITED, Hint::LIST ); |
| 309 | } |
| 310 | return parent::jsonClassHintFor( $keyName ); |
| 311 | } |
| 312 | } |