Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
66.67% covered (warning)
66.67%
24 / 36
76.92% covered (warning)
76.92%
10 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
StripState
66.67% covered (warning)
66.67%
24 / 36
76.92% covered (warning)
76.92%
10 / 13
25.48
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 __clone
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 newKey
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 isEmpty
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addWtItem
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 containsStripMarker
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 startsWithStripMarker
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 endsWithStripMarker
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addWtItemKey
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 splitWt
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 new
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addAllFrom
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 merge
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\Parsoid\Fragments;
5
6use Wikimedia\Assert\Assert;
7
8/**
9 * An abstraction/generalization of "strip state" from mediawiki core.
10 *
11 * The basic idea is that a special "strip marker" can be added to a text
12 * string to represent the insertion of a fragment, here represented as a
13 * PFragment (in core, represented as HTML).  This allows us to tunnel
14 * rich content through interfaces which only allow strings, as long as
15 * (a) we can maintain a strip state on the side, and (b) we guarantee
16 * that the "strip markers" can never be forged in the string.  For
17 * strip markers in wikitext and HTML this is guaranteed by using
18 * a character (\x7f) which is invalid in both wikitext and HTML.
19 *
20 * The StripState object is not serializable because we can't easily
21 * enforce the uniqueness of strip state keys on deserialization.
22 * It is recommended that wikitext+strip state be serialized using
23 * ::splitWt() (ie, as an array alternating between wikitext strings
24 * and serialized PFragments) which both avoids the need to serialize
25 * the strip state itself and also avoids exposing the internal keys
26 * in the serialized representation.
27 *
28 * StripState should generally be considered an opaque type internal
29 * to Parsoid; most external clients should use the appropriate
30 * `split` methods to yield a list of Fragments rather than directly
31 * interact with strip markers.
32 */
33class StripState {
34
35    /**
36     * See Parser.php::MARKER_SUFFIX in core for an explanation of the
37     * special characters used in the marker.
38     *
39     * @note This marker is only valid in strings! We would need to
40     * use an alternate marker if we wanted "strip markers" inside DOM
41     * content since \x7f is (deliberately) not a valid HTML
42     * character.
43     *
44     * @note These markers are generally not visible outside of Parsoid;
45     * they are replaced with "real" core strip markers before being
46     * passed to legacy code.  *However* when running Parsoid in
47     * standalone/"API" mode we do use these to bypass fragment content
48     * around the legacy preprocessor, and so these should *not* match
49     * the Parser::MARKER_PREFIX used in core.  We've added a `P` to
50     * our prefix/suffix to ensure we don't conflict.
51     */
52    private const MARKER_PREFIX = "\x7f'\"`PUNIQ-";
53    /** @see ::MARKER_PREFIX */
54    private const MARKER_SUFFIX = "-QINUP`\"'\x7f";
55
56    /**
57     * The global strip state counter is guaranteed to be greater than
58     * the major counters in any created strip state.
59     */
60    private static int $stripStateCounter = 0;
61
62    private int $majorCounter;
63
64    /**
65     * The minor counter for a strip state is guaranteed to be greater than the
66     * minor counter for all items in the strip state *with the same major
67     * counter*.
68     */
69    private int $minorCounter;
70
71    /**
72     * @var array<string,PFragment> A mapping from strip state keys to
73     *  PFragments.
74     */
75    private array $items = [];
76
77    private function __construct() {
78        $this->majorCounter = self::$stripStateCounter++;
79        $this->minorCounter = 0;
80    }
81
82    public function __clone() {
83        // Ensure no two strip states have the same major counter
84        $this->majorCounter = self::$stripStateCounter++;
85        $this->minorCounter = 0;
86    }
87
88    /**
89     * Create a new internal key, guaranteed not to conflict with any other
90     * key.
91     */
92    private function newKey(): string {
93        $major = $this->majorCounter;
94        $minor = $this->minorCounter++;
95        return "$major-$minor";
96    }
97
98    /** Return true if there are no items in this strip state. */
99    public function isEmpty(): bool {
100        return !$this->items;
101    }
102
103    /**
104     * Add the given fragment to the strip state, returning a wikitext
105     * string that can be used as a placeholder for it.
106     */
107    public function addWtItem( PFragment $fragment ): string {
108        Assert::invariant(
109            !( $fragment instanceof WikitextPFragment ),
110            "strip state items should not be wikitext"
111        );
112        $key = $this->addWtItemKey( $fragment );
113        return self::MARKER_PREFIX . $key . self::MARKER_SUFFIX;
114    }
115
116    /**
117     * Return true if the given wikitext string contains a strip marker,
118     * or false otherwise.
119     */
120    public static function containsStripMarker( string $s ): bool {
121        return str_contains( $s, self::MARKER_PREFIX );
122    }
123
124    /**
125     * Return true if the given wikitext string starts with a strip marker,
126     * or false otherwise.
127     */
128    public static function startsWithStripMarker( string $s ): bool {
129        return str_starts_with( $s, self::MARKER_PREFIX );
130    }
131
132    /**
133     * Return true if the given wikitext string ends with a strip marker,
134     * or false otherwise.
135     */
136    public static function endsWithStripMarker( string $s ): bool {
137        return str_ends_with( $s, self::MARKER_SUFFIX );
138    }
139
140    /**
141     * Add the given fragment to the strip state, returning the internal
142     * strip state key used for it.
143     */
144    private function addWtItemKey( PFragment $fragment ): string {
145        Assert::invariant(
146            !( $fragment instanceof WikitextPFragment ),
147            "wikitext fragments shouldn't be buried in strip state"
148        );
149        $key = $this->newKey();
150        $this->items[$key] = $fragment;
151        return $key;
152    }
153
154    /**
155     * Split the given wikitext string at its strip markers and return an array
156     * which alternates between string items and PFragment items.
157     * The first and last items are guaranteed to be strings, and the
158     * array length is guaranteed to be odd and at least 1.
159     * @return list<string|PFragment>
160     */
161    public function splitWt( string $wikitext ): array {
162        static $regex = '/' . self::MARKER_PREFIX . "([^\x7f<>&'\"]+)" . self::MARKER_SUFFIX . '/';
163        $pieces = preg_split( $regex, $wikitext, -1, PREG_SPLIT_DELIM_CAPTURE );
164        for ( $i = 1; $i < count( $pieces ); $i += 2 ) {
165            $pieces[$i] = $this->items[$pieces[$i]];
166        }
167        return $pieces;
168    }
169
170    /**
171     * Create a new empty strip state.
172     */
173    public static function new(): StripState {
174        return new StripState();
175    }
176
177    /**
178     * Add all mappings from the given strip states to this one.
179     */
180    public function addAllFrom( StripState ...$others ): void {
181        foreach ( $others as $ss ) {
182            foreach ( $ss->items as $key => $value ) {
183                $this->items[$key] = $value;
184            }
185        }
186    }
187
188    /**
189     * Create a new strip state which contains the mappings of all of the
190     * given strip states.
191     */
192    public static function merge( StripState $first, StripState ...$others ): StripState {
193        $ss = clone $first;
194        $ss->addAllFrom( ...$others );
195        return $ss;
196    }
197}