Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
54.08% covered (warning)
54.08%
53 / 98
29.41% covered (danger)
29.41%
5 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
WikitextPFragment
54.08% covered (warning)
54.08%
53 / 98
29.41% covered (danger)
29.41%
5 / 17
231.44
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 newFromWt
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 newFromSplitWt
65.00% covered (warning)
65.00%
26 / 40
0.00% covered (danger)
0.00%
0 / 1
31.89
 newFromLiteral
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 castFromPFragment
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 isEmpty
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isAtomic
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 asDom
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 asMarkedWikitext
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 startsWithMarker
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 endsWithMarker
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 split
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 trim
64.71% covered (warning)
64.71%
11 / 17
0.00% covered (danger)
0.00%
0 / 1
4.70
 concat
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 toJsonArray
60.00% covered (warning)
60.00%
6 / 10
0.00% covered (danger)
0.00%
0 / 1
2.26
 newFromJsonArray
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 jsonClassHintFor
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\Parsoid\Fragments;
5
6use Wikimedia\JsonCodec\Hint;
7use Wikimedia\Parsoid\Core\DomSourceRange;
8use Wikimedia\Parsoid\DOM\DocumentFragment;
9use Wikimedia\Parsoid\Ext\ParsoidExtensionAPI;
10use 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 */
24class 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}