Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 229 |
|
0.00% |
0 / 16 |
CRAP | |
0.00% |
0 / 1 |
| ListHandler | |
0.00% |
0 / 229 |
|
0.00% |
0 / 16 |
6162 | |
0.00% |
0 / 1 |
| __construct | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| getListFrame | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| generateImpliedEndTags | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| reset | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
| onTag | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
| onCompoundTk | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
| onNewline | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
20 | |||
| onAny | |
0.00% |
0 / 52 |
|
0.00% |
0 / 1 |
650 | |||
| popListFrame | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
| onEnd | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
| closeLists | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
| onListItem | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
56 | |||
| commonPrefixLength | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
| isDtDd | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
| makeDP | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
| doListItem | |
0.00% |
0 / 84 |
|
0.00% |
0 / 1 |
342 | |||
| 1 | <?php |
| 2 | declare( strict_types = 1 ); |
| 3 | |
| 4 | namespace Wikimedia\Parsoid\Wt2Html\TT; |
| 5 | |
| 6 | use Wikimedia\Assert\UnreachableException; |
| 7 | use Wikimedia\Parsoid\Core\SourceRange; |
| 8 | use Wikimedia\Parsoid\NodeData\DataParsoid; |
| 9 | use Wikimedia\Parsoid\Tokens\CompoundTk; |
| 10 | use Wikimedia\Parsoid\Tokens\EmptyLineTk; |
| 11 | use Wikimedia\Parsoid\Tokens\EndTagTk; |
| 12 | use Wikimedia\Parsoid\Tokens\EOFTk; |
| 13 | use Wikimedia\Parsoid\Tokens\IndentPreTk; |
| 14 | use Wikimedia\Parsoid\Tokens\TagTk; |
| 15 | use Wikimedia\Parsoid\Tokens\Token; |
| 16 | use Wikimedia\Parsoid\Tokens\XMLTagTk; |
| 17 | use Wikimedia\Parsoid\Utils\PHPUtils; |
| 18 | use Wikimedia\Parsoid\Utils\TokenUtils; |
| 19 | use Wikimedia\Parsoid\Wt2Html\TokenHandlerPipeline; |
| 20 | |
| 21 | /** |
| 22 | * Create list tag around list items and map wiki bullet levels to html. |
| 23 | */ |
| 24 | class ListHandler extends LineBasedHandler { |
| 25 | /** |
| 26 | * Debug string output of bullet character mappings. |
| 27 | * @var array<string,array<string,string>> |
| 28 | */ |
| 29 | private static $bullet_chars_map = [ |
| 30 | '*' => [ 'list' => 'ul', 'item' => 'li' ], |
| 31 | '#' => [ 'list' => 'ol', 'item' => 'li' ], |
| 32 | ';' => [ 'list' => 'dl', 'item' => 'dt' ], |
| 33 | ':' => [ 'list' => 'dl', 'item' => 'dd' ] |
| 34 | ]; |
| 35 | |
| 36 | /** @var array<ListFrame> */ |
| 37 | private array $listFrameStack; |
| 38 | private int $nestedTableCount; |
| 39 | private bool $inT2529Mode = false; |
| 40 | |
| 41 | public function __construct( TokenHandlerPipeline $manager, array $options ) { |
| 42 | parent::__construct( $manager, $options ); |
| 43 | $this->reset(); |
| 44 | } |
| 45 | |
| 46 | private bool $haveActiveListFrame = false; |
| 47 | |
| 48 | private function getListFrame(): ListFrame { |
| 49 | return $this->listFrameStack[count( $this->listFrameStack ) - 1]; |
| 50 | } |
| 51 | |
| 52 | /** |
| 53 | * The HTML5 parsing spec says that when encountering a closing tag for a |
| 54 | * certain set of open tags we should generate implied ends to list items, |
| 55 | * https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inbody:generate-implied-end-tags-5 |
| 56 | * |
| 57 | * So, in order to roundtrip accurately, we should follow suit. However, |
| 58 | * we choose an ostensible superset of those tags, our wikitext blocks, to |
| 59 | * have this behaviour. Hopefully the differences aren't relevant. |
| 60 | * |
| 61 | * @param string $tagName |
| 62 | * @return bool |
| 63 | */ |
| 64 | private function generateImpliedEndTags( string $tagName ): bool { |
| 65 | return TokenUtils::isWikitextBlockTag( $tagName ); |
| 66 | } |
| 67 | |
| 68 | /** |
| 69 | * Resets the list handler |
| 70 | */ |
| 71 | private function reset(): void { |
| 72 | $this->listFrameStack = []; |
| 73 | $this->onAnyEnabled = false; |
| 74 | $this->nestedTableCount = 0; |
| 75 | $this->haveActiveListFrame = false; |
| 76 | } |
| 77 | |
| 78 | /** |
| 79 | * @inheritDoc |
| 80 | */ |
| 81 | public function onTag( XMLTagTk $token ): ?array { |
| 82 | if ( $token->getName() === 'listItem' ) { |
| 83 | '@phan-var TagTk $token'; // @var TagTk $token |
| 84 | return $this->onListItem( $token ); |
| 85 | } else { |
| 86 | return null; |
| 87 | } |
| 88 | } |
| 89 | |
| 90 | /** |
| 91 | * @inheritDoc |
| 92 | */ |
| 93 | public function onCompoundTk( CompoundTk $ctk, TokenHandler $tokensHandler ): ?array { |
| 94 | if ( $ctk instanceof EmptyLineTk || $ctk instanceof IndentPreTk ) { |
| 95 | // Nothing to do! |
| 96 | // IndentPre content / empty lines are of no interest to us |
| 97 | return null; |
| 98 | } else { |
| 99 | throw new UnreachableException( |
| 100 | "ListHandler: Unsupported compound token." |
| 101 | ); |
| 102 | } |
| 103 | } |
| 104 | |
| 105 | /** |
| 106 | * @inheritDoc |
| 107 | */ |
| 108 | public function onNewline( $token ): ?array { |
| 109 | $this->env->trace( 'list', $this->pipelineId, 'NEWLINE:', $token ); |
| 110 | |
| 111 | if ( !$this->onAnyEnabled ) { |
| 112 | return null; |
| 113 | } |
| 114 | |
| 115 | // onAny handler is only active when there's at least one list frame |
| 116 | // on the stack, even if it is not active |
| 117 | $listFrame = $this->getListFrame(); |
| 118 | |
| 119 | if ( !$this->haveActiveListFrame ) { |
| 120 | $listFrame->listTk->addToken( $token ); |
| 121 | $this->env->trace( 'list', $this->pipelineId, 'RET[LIST]:', $token ); |
| 122 | return []; |
| 123 | } |
| 124 | |
| 125 | if ( $listFrame->atEOL ) { |
| 126 | // Non-list item in newline context |
| 127 | // ==> close all previous lists and reset frame to null |
| 128 | return $this->closeLists( $listFrame, $token ); |
| 129 | } else { |
| 130 | $listFrame->atEOL = true; |
| 131 | $listFrame->nlTk = $token; |
| 132 | $listFrame->haveDD = false; |
| 133 | // php's findColonNoLinks is run in doBlockLevels, which examines |
| 134 | // the text line-by-line. At nltk, any open tags will cease having |
| 135 | // an effect. |
| 136 | $listFrame->numOpenTags = 0; |
| 137 | return []; |
| 138 | } |
| 139 | } |
| 140 | |
| 141 | /** |
| 142 | * @inheritDoc |
| 143 | */ |
| 144 | public function onAny( $token ): ?array { |
| 145 | $this->env->trace( 'list', $this->pipelineId, 'ANY:', $token ); |
| 146 | |
| 147 | if ( $token instanceof Token && TokenUtils::matchTypeOf( $token, '#^mw:Transclusion$#' ) ) { |
| 148 | // We are now in T2529 scenario where legacy would have added a newline |
| 149 | // if it had encountered a list-start character. So, if we encounter |
| 150 | // a listItem token next, we should first execute the same actions |
| 151 | // as if we had run onAny on a NlTk (see below). |
| 152 | $this->inT2529Mode = true; |
| 153 | } elseif ( !TokenUtils::isSolTransparent( $this->env, $token ) ) { |
| 154 | $this->inT2529Mode = false; |
| 155 | } |
| 156 | |
| 157 | // onAny handler is only active when there's at least one list frame |
| 158 | // on the stack, even if it is not active |
| 159 | $listFrame = $this->getListFrame(); |
| 160 | |
| 161 | if ( !$this->haveActiveListFrame ) { |
| 162 | // haveActiveListFrame will be false in the onAny handler only when |
| 163 | // we are in a table that in turn was seen in a list context. |
| 164 | // |
| 165 | // Since we are not in a list within the table, nothing to do. |
| 166 | // Just stuff it in the list token. |
| 167 | if ( $token instanceof EndTagTk && $token->getName() === 'table' ) { |
| 168 | if ( $this->nestedTableCount === 0 ) { |
| 169 | $this->haveActiveListFrame = true; |
| 170 | } else { |
| 171 | $this->nestedTableCount--; |
| 172 | } |
| 173 | } elseif ( $token instanceof TagTk && $token->getName() === 'table' ) { |
| 174 | $this->nestedTableCount++; |
| 175 | } |
| 176 | |
| 177 | $listFrame->listTk->addToken( $token ); |
| 178 | $this->env->trace( 'list', $this->pipelineId, 'RET[LIST]:', $token ); |
| 179 | return []; |
| 180 | } |
| 181 | |
| 182 | // Keep track of open tags per list frame in order to prevent colons |
| 183 | // starting lists illegally. Php's findColonNoLinks. |
| 184 | if ( $token instanceof TagTk |
| 185 | // Table tokens will push the frame and remain balanced. |
| 186 | // They're safe to ignore in the bookkeeping. |
| 187 | && $token->getName() !== 'table' |
| 188 | ) { |
| 189 | $listFrame->numOpenTags += 1; |
| 190 | } elseif ( $token instanceof EndTagTk ) { |
| 191 | if ( $listFrame->numOpenTags > 0 ) { |
| 192 | $listFrame->numOpenTags -= 1; |
| 193 | } |
| 194 | |
| 195 | if ( $token->getName() === 'table' ) { |
| 196 | // close all open lists and pop a frame |
| 197 | $ret = $this->closeLists( $listFrame, $token ); |
| 198 | if ( count( $this->listFrameStack ) > 0 ) { |
| 199 | $this->haveActiveListFrame = true; |
| 200 | } |
| 201 | return $ret; |
| 202 | } elseif ( $this->generateImpliedEndTags( $token->getName() ) ) { |
| 203 | if ( $listFrame->numOpenBlockTags === 0 ) { |
| 204 | // Unbalanced closing block tag in a list context |
| 205 | // ==> close all previous lists and reset frame to null |
| 206 | return $this->closeLists( $listFrame, $token ); |
| 207 | } else { |
| 208 | $listFrame->numOpenBlockTags--; |
| 209 | if ( $listFrame->atEOL ) { |
| 210 | // Non-list item in newline context |
| 211 | // ==> close all previous lists and reset frame to null |
| 212 | return $this->closeLists( $listFrame, $token ); |
| 213 | } else { |
| 214 | $listFrame->listTk->addToken( $token ); |
| 215 | $this->env->trace( 'list', $this->pipelineId, 'RET[LIST]:', $token ); |
| 216 | return []; |
| 217 | } |
| 218 | } |
| 219 | } |
| 220 | |
| 221 | /* Non-block tag -- fall-through to other tests below */ |
| 222 | } |
| 223 | |
| 224 | if ( $listFrame->atEOL ) { |
| 225 | if ( TokenUtils::isSolTransparent( $this->env, $token ) ) { |
| 226 | // Hold on to see where the token stream goes from here |
| 227 | // - another list item, or |
| 228 | // - end of list |
| 229 | if ( $listFrame->nlTk ) { |
| 230 | $listFrame->solTokens[] = $listFrame->nlTk; |
| 231 | $listFrame->nlTk = null; |
| 232 | } |
| 233 | $listFrame->solTokens[] = $token; |
| 234 | return []; |
| 235 | } else { |
| 236 | // Non-list item in newline context |
| 237 | // ==> close all previous lists and reset frame to null |
| 238 | return $this->closeLists( $listFrame, $token ); |
| 239 | } |
| 240 | } |
| 241 | |
| 242 | if ( $token instanceof TagTk ) { |
| 243 | if ( $token->getName() === 'table' ) { |
| 244 | $this->haveActiveListFrame = false; |
| 245 | } elseif ( $this->generateImpliedEndTags( $token->getName() ) ) { |
| 246 | $listFrame->numOpenBlockTags++; |
| 247 | } |
| 248 | } |
| 249 | |
| 250 | // Nothing else left to do |
| 251 | $listFrame->listTk->addToken( $token ); |
| 252 | $this->env->trace( 'list', $this->pipelineId, 'RET[LIST]:', $token ); |
| 253 | return []; |
| 254 | } |
| 255 | |
| 256 | /** |
| 257 | * @param array<string|Token> $ret |
| 258 | * @return array<string|Token> |
| 259 | */ |
| 260 | private function popListFrame( array $ret ): array { |
| 261 | array_pop( $this->listFrameStack ); |
| 262 | $this->haveActiveListFrame = false; |
| 263 | |
| 264 | if ( count( $this->listFrameStack ) > 0 ) { |
| 265 | $this->getListFrame()->listTk->addTokens( $ret ); |
| 266 | $this->env->trace( 'list', $this->pipelineId, 'RET[LIST]:', $ret ); |
| 267 | $ret = []; |
| 268 | } else { |
| 269 | // Remove onAny transform if we dont have any stashed list frames |
| 270 | $this->onAnyEnabled = false; |
| 271 | $this->env->trace( 'list', $this->pipelineId, 'RET:', $ret ); |
| 272 | } |
| 273 | |
| 274 | return $ret; |
| 275 | } |
| 276 | |
| 277 | /** |
| 278 | * @inheritDoc |
| 279 | */ |
| 280 | public function onEnd( EOFTk $token ): ?array { |
| 281 | $this->env->trace( 'list', $this->pipelineId, 'END:', $token ); |
| 282 | |
| 283 | if ( $this->haveActiveListFrame ) { |
| 284 | // EOF goes at the end outside all compound tokens |
| 285 | // So, don't pass in the token into closeLists |
| 286 | $toks = $this->closeLists( $this->getListFrame() ); |
| 287 | } else { |
| 288 | // all lists have been closed and nothing to do on that front |
| 289 | $toks = []; |
| 290 | } |
| 291 | |
| 292 | while ( count( $this->listFrameStack ) > 0 ) { |
| 293 | // $toks should be [] here |
| 294 | $toks = $this->popListFrame( [ $this->getListFrame()->listTk ] ); |
| 295 | } |
| 296 | $this->reset(); |
| 297 | |
| 298 | $toks[] = $token; |
| 299 | $this->env->trace( 'list', $this->pipelineId, 'RET: ', $toks ); |
| 300 | return $toks; |
| 301 | } |
| 302 | |
| 303 | /** |
| 304 | * Handle close list processing |
| 305 | * |
| 306 | * @param ListFrame $listFrame |
| 307 | * @param Token|string|null $token |
| 308 | * @return array<string|Token> |
| 309 | */ |
| 310 | private function closeLists( ListFrame $listFrame, $token = null ): array { |
| 311 | $this->env->trace( 'list', $this->pipelineId, '----closing all lists----' ); |
| 312 | |
| 313 | // pop all open list item tokens onto $listFrame->listTk |
| 314 | $ret = $listFrame->popTags( count( $listFrame->bstack ) ); |
| 315 | $listFrame->listTk->addTokens( $ret ); |
| 316 | $this->env->trace( 'list', $this->pipelineId, 'RET[LIST]: ', $ret ); |
| 317 | |
| 318 | $ret = [ $listFrame->listTk ]; |
| 319 | PHPUtils::pushArray( $ret, $listFrame->solTokens ); |
| 320 | if ( $listFrame->nlTk ) { |
| 321 | $ret[] = $listFrame->nlTk; |
| 322 | } |
| 323 | if ( $token ) { |
| 324 | $ret[] = $token; |
| 325 | } |
| 326 | |
| 327 | return $this->popListFrame( $ret ); |
| 328 | } |
| 329 | |
| 330 | /** |
| 331 | * Handle a list item |
| 332 | * @return ?array<string|Token> |
| 333 | */ |
| 334 | private function onListItem( TagTk $token ): ?array { |
| 335 | if ( $this->inT2529Mode ) { |
| 336 | // See comment in onAny where this property is set to true |
| 337 | // The only relevant change is to 'haveDD'. |
| 338 | if ( $this->haveActiveListFrame ) { |
| 339 | $this->getListFrame()->haveDD = false; |
| 340 | } |
| 341 | $this->inT2529Mode = false; |
| 342 | |
| 343 | // 'atEOL' and NlTk changes don't apply. |
| 344 | // |
| 345 | // This might be a divergence, but, I don't think we should |
| 346 | // close open tags here as in the NlTk case. So, this means that |
| 347 | // this wikitext ";term: def=foo<b>{{1x|:bar}}</b>" |
| 348 | // will generate different output in Parsoid & legacy.. |
| 349 | // I believe Parsoid's output is better, but we can comply |
| 350 | // if we see a real regression for this. |
| 351 | } |
| 352 | |
| 353 | $this->onAnyEnabled = true; |
| 354 | $bullets = $token->getAttributeV( 'bullets' ); |
| 355 | if ( $this->haveActiveListFrame ) { |
| 356 | $listFrame = $this->getListFrame(); |
| 357 | // Ignoring colons inside tags to prevent illegal overlapping. |
| 358 | // Attempts to mimic findColonNoLinks in the php parser. |
| 359 | if ( |
| 360 | PHPUtils::lastItem( $bullets ) === ':' && |
| 361 | ( $listFrame->haveDD || $listFrame->numOpenTags > 0 ) |
| 362 | ) { |
| 363 | $this->env->trace( 'list', $this->pipelineId, 'ANY:', $token ); |
| 364 | $this->env->trace( 'list', $this->pipelineId, 'RET[LIST]: ', ':' ); |
| 365 | $listFrame->listTk->addToken( ':' ); |
| 366 | return []; |
| 367 | } |
| 368 | } else { |
| 369 | $listFrame = new ListFrame; |
| 370 | $this->listFrameStack[] = $listFrame; |
| 371 | $this->haveActiveListFrame = true; |
| 372 | } |
| 373 | // convert listItem to list and list item tokens |
| 374 | return $this->doListItem( $listFrame, $bullets, $token ); |
| 375 | } |
| 376 | |
| 377 | /** |
| 378 | * Determine the minimum common prefix length |
| 379 | */ |
| 380 | private function commonPrefixLength( array $x, array $y ): int { |
| 381 | $minLength = min( count( $x ), count( $y ) ); |
| 382 | $i = 0; |
| 383 | for ( ; $i < $minLength; $i++ ) { |
| 384 | if ( $x[$i] !== $y[$i] ) { |
| 385 | break; |
| 386 | } |
| 387 | } |
| 388 | return $i; |
| 389 | } |
| 390 | |
| 391 | /** Check for Dt Dd sequence */ |
| 392 | private static function isDtDd( string $a, string $b ): bool { |
| 393 | $ab = [ $a, $b ]; |
| 394 | sort( $ab ); |
| 395 | return ( $ab[0] === ':' && $ab[1] === ';' ); |
| 396 | } |
| 397 | |
| 398 | /** Make a new DP that is a slice of a source DP */ |
| 399 | private function makeDP( DataParsoid $sourceDP, int $startOffset, int $endOffset ): DataParsoid { |
| 400 | $newDP = clone $sourceDP; |
| 401 | $tsr = $sourceDP->tsr ?? null; |
| 402 | if ( $tsr ) { |
| 403 | $newDP->tsr = new SourceRange( $tsr->start + $startOffset, $tsr->start + $endOffset, $tsr->source ); |
| 404 | } |
| 405 | return $newDP; |
| 406 | } |
| 407 | |
| 408 | /** |
| 409 | * Handle list item processing |
| 410 | * |
| 411 | * @param ListFrame $listFrame |
| 412 | * @param array $bn |
| 413 | * @param Token $token |
| 414 | * @return array<string|Token> |
| 415 | */ |
| 416 | private function doListItem( ListFrame $listFrame, array $bn, Token $token ): array { |
| 417 | $this->env->trace( 'list', $this->pipelineId, 'BEGIN:', $token ); |
| 418 | |
| 419 | $bs = $listFrame->bstack; |
| 420 | $prefixLen = $this->commonPrefixLength( $bs, $bn ); |
| 421 | $prefix = array_slice( $bn, 0, $prefixLen/*CHECK THIS*/ ); |
| 422 | $tokenDP = $token->dataParsoid; |
| 423 | $listFrame->bstack = $bn; |
| 424 | |
| 425 | // emit close tag tokens for closed lists |
| 426 | $this->env->trace( |
| 427 | 'list', $this->pipelineId, |
| 428 | static function () use ( $bs, $bn ) { |
| 429 | return ' bs: ' . PHPUtils::jsonEncode( $bs ) . '; bn: ' . PHPUtils::jsonEncode( $bn ); |
| 430 | } |
| 431 | ); |
| 432 | |
| 433 | if ( count( $prefix ) === count( $bs ) && count( $bn ) === count( $bs ) ) { |
| 434 | $this->env->trace( 'list', $this->pipelineId, ' -> no nesting change' ); |
| 435 | |
| 436 | // same list item types and same nesting level |
| 437 | $itemToken = array_pop( $listFrame->endtags ); |
| 438 | $listFrame->endtags[] = new EndTagTk( $itemToken->getName() ); |
| 439 | $res = array_merge( [ $itemToken ], |
| 440 | $listFrame->solTokens, |
| 441 | [ |
| 442 | // this list item gets all the bullets since this is |
| 443 | // a list item at the same level |
| 444 | // |
| 445 | // **a |
| 446 | // **b |
| 447 | $listFrame->nlTk ?: '', |
| 448 | new TagTk( $itemToken->getName(), [], $this->makeDP( $tokenDP, 0, count( $bn ) ) ) |
| 449 | ] |
| 450 | ); |
| 451 | } else { |
| 452 | $prefixCorrection = 0; |
| 453 | $tokens = []; |
| 454 | if ( count( $bs ) > $prefixLen |
| 455 | && count( $bn ) > $prefixLen |
| 456 | && self::isDtDd( $bs[$prefixLen], $bn[$prefixLen] ) ) { |
| 457 | /* ------------------------------------------------ |
| 458 | * Handle dd/dt transitions |
| 459 | * |
| 460 | * Example: |
| 461 | * |
| 462 | * **;:: foo |
| 463 | * **::: bar |
| 464 | * |
| 465 | * the 3rd bullet is the dt-dd transition |
| 466 | * ------------------------------------------------ */ |
| 467 | |
| 468 | $tokens = $listFrame->popTags( count( $bs ) - $prefixLen - 1 ); |
| 469 | $tokens = array_merge( $listFrame->solTokens, $tokens ); |
| 470 | $newName = self::$bullet_chars_map[$bn[$prefixLen]]['item']; |
| 471 | $endTag = array_pop( $listFrame->endtags ); |
| 472 | if ( $newName === 'dd' ) { |
| 473 | $listFrame->haveDD = true; |
| 474 | } elseif ( $newName === 'dt' ) { |
| 475 | $listFrame->haveDD = false; // reset |
| 476 | } |
| 477 | $listFrame->endtags[] = new EndTagTk( $newName ); |
| 478 | |
| 479 | if ( isset( $tokenDP->stx ) && $tokenDP->stx === 'row' ) { |
| 480 | // stx='row' is only set for single-line dt-dd lists (see tokenizer) |
| 481 | // In this scenario, the dd token we are building a token for has no prefix |
| 482 | // Ex: ;a:b, *;a:b, #**;a:b, etc. Compare with *;a\n*:b, #**;a\n#**:b |
| 483 | $this->env->trace( 'list', $this->pipelineId, |
| 484 | ' -> single-line dt->dd transition' ); |
| 485 | $newTag = new TagTk( $newName, [], $this->makeDP( $tokenDP, 0, 1 ) ); |
| 486 | } else { |
| 487 | $this->env->trace( 'list', $this->pipelineId, ' -> other dt/dd transition' ); |
| 488 | $newTag = new TagTk( $newName, [], $this->makeDP( $tokenDP, 0, $prefixLen + 1 ) ); |
| 489 | } |
| 490 | |
| 491 | $tokens[] = $endTag; |
| 492 | $tokens[] = $listFrame->nlTk ?: ''; |
| 493 | $tokens[] = $newTag; |
| 494 | |
| 495 | $prefixCorrection = 1; |
| 496 | } else { |
| 497 | $this->env->trace( 'list', $this->pipelineId, ' -> reduced nesting' ); |
| 498 | $tokens = array_merge( |
| 499 | $listFrame->solTokens, |
| 500 | $tokens, |
| 501 | $listFrame->popTags( count( $bs ) - $prefixLen ) |
| 502 | ); |
| 503 | if ( $listFrame->nlTk ) { |
| 504 | $tokens[] = $listFrame->nlTk; |
| 505 | } |
| 506 | if ( $prefixLen > 0 && count( $bn ) === $prefixLen ) { |
| 507 | $itemToken = array_pop( $listFrame->endtags ); |
| 508 | $tokens[] = $itemToken; |
| 509 | // this list item gets all bullets upto the shared prefix |
| 510 | $tokens[] = new TagTk( $itemToken->getName(), [], $this->makeDP( $tokenDP, 0, count( $bn ) ) ); |
| 511 | $listFrame->endtags[] = new EndTagTk( $itemToken->getName() ); |
| 512 | } |
| 513 | } |
| 514 | |
| 515 | for ( $i = $prefixLen + $prefixCorrection; $i < count( $bn ); $i++ ) { |
| 516 | if ( !self::$bullet_chars_map[$bn[$i]] ) { |
| 517 | throw new \InvalidArgumentException( 'Unknown node prefix ' . $prefix[$i] ); |
| 518 | } |
| 519 | |
| 520 | // Each list item in the chain gets one bullet. |
| 521 | // However, the first item also includes the shared prefix. |
| 522 | // |
| 523 | // Example: |
| 524 | // |
| 525 | // **a |
| 526 | // ****b |
| 527 | // |
| 528 | // Yields: |
| 529 | // |
| 530 | // <ul><li-*> |
| 531 | // <ul><li-*>a |
| 532 | // <ul><li-FIRST-ONE-gets-***> |
| 533 | // <ul><li-*>b</li></ul> |
| 534 | // </li></ul> |
| 535 | // </li></ul> |
| 536 | // </li></ul> |
| 537 | // |
| 538 | // Unless prefixCorrection is > 0, in which case we've |
| 539 | // already accounted for the initial bullets. |
| 540 | // |
| 541 | // prefixCorrection is for handling dl-dts like this |
| 542 | // |
| 543 | // ;a:b |
| 544 | // ;;c:d |
| 545 | // |
| 546 | // ";c:d" is embedded within a dt that is 1 char wide(;) |
| 547 | |
| 548 | if ( $i === $prefixLen ) { |
| 549 | $this->env->trace( 'list', $this->pipelineId, |
| 550 | ' -> increased nesting: first' |
| 551 | ); |
| 552 | $listDP = $this->makeDP( $tokenDP, 0, 0 ); |
| 553 | $listItemDP = $this->makeDP( $tokenDP, 0, $i + 1 ); |
| 554 | } else { |
| 555 | $this->env->trace( 'list', $this->pipelineId, |
| 556 | ' -> increased nesting: 2nd and higher' |
| 557 | ); |
| 558 | $listDP = $this->makeDP( $tokenDP, $i, $i ); |
| 559 | $listItemDP = $this->makeDP( $tokenDP, $i, $i + 1 ); |
| 560 | } |
| 561 | |
| 562 | PHPUtils::pushArray( $tokens, $listFrame->pushList( |
| 563 | self::$bullet_chars_map[$bn[$i]], $listDP, $listItemDP |
| 564 | ) ); |
| 565 | } |
| 566 | $res = $tokens; |
| 567 | } |
| 568 | |
| 569 | // clear out sol-tokens |
| 570 | $listFrame->solTokens = []; |
| 571 | $listFrame->nlTk = null; |
| 572 | $listFrame->atEOL = false; |
| 573 | $listFrame->listTk->addTokens( $res ); |
| 574 | |
| 575 | $this->env->trace( 'list', $this->pipelineId, 'RET[LIST]:', $res ); |
| 576 | return []; |
| 577 | } |
| 578 | } |