Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
66.67% |
24 / 36 |
|
76.92% |
10 / 13 |
CRAP | |
0.00% |
0 / 1 |
| StripState | |
66.67% |
24 / 36 |
|
76.92% |
10 / 13 |
25.48 | |
0.00% |
0 / 1 |
| __construct | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| __clone | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| newKey | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
| isEmpty | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| addWtItem | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
| containsStripMarker | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| startsWithStripMarker | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| endsWithStripMarker | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| addWtItemKey | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
| splitWt | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
| new | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| addAllFrom | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| merge | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | declare( strict_types = 1 ); |
| 3 | |
| 4 | namespace Wikimedia\Parsoid\Fragments; |
| 5 | |
| 6 | use 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 | */ |
| 33 | class 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 | } |