Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 96 |
|
0.00% |
0 / 8 |
CRAP | |
0.00% |
0 / 1 |
| PHandler | |
0.00% |
0 / 96 |
|
0.00% |
0 / 8 |
4556 | |
0.00% |
0 / 1 |
| __construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| handle | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| before | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
462 | |||
| after | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
156 | |||
| currWikitextLineHasBlockNode | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
132 | |||
| newWikitextLineMightHaveBlockNode | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
72 | |||
| treatAsPPTransition | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
72 | |||
| isPPTransition | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
30 | |||
| 1 | <?php |
| 2 | declare( strict_types = 1 ); |
| 3 | |
| 4 | namespace Wikimedia\Parsoid\Html2Wt\DOMHandlers; |
| 5 | |
| 6 | use stdClass; |
| 7 | use Wikimedia\Parsoid\DOM\Element; |
| 8 | use Wikimedia\Parsoid\DOM\Node; |
| 9 | use Wikimedia\Parsoid\DOM\Text; |
| 10 | use Wikimedia\Parsoid\Html2Wt\SerializerState; |
| 11 | use Wikimedia\Parsoid\Utils\DiffDOMUtils; |
| 12 | use Wikimedia\Parsoid\Utils\DOMCompat; |
| 13 | use Wikimedia\Parsoid\Utils\DOMDataUtils; |
| 14 | use Wikimedia\Parsoid\Utils\DOMUtils; |
| 15 | use Wikimedia\Parsoid\Utils\WTUtils; |
| 16 | use Wikimedia\Parsoid\Wikitext\Consts; |
| 17 | |
| 18 | class PHandler extends DOMHandler { |
| 19 | |
| 20 | public function __construct() { |
| 21 | // Counterintuitive but seems right. |
| 22 | // Otherwise the generated wikitext will parse as an indent-pre |
| 23 | // escapeWikitext nowiking will deal with leading space for content |
| 24 | // inside the p-tag, but forceSOL suppresses whitespace before the p-tag. |
| 25 | parent::__construct( true ); |
| 26 | } |
| 27 | |
| 28 | /** @inheritDoc */ |
| 29 | public function handle( |
| 30 | Element $node, SerializerState $state, bool $wrapperUnmodified = false |
| 31 | ): ?Node { |
| 32 | // XXX: Handle single-line mode by switching to HTML handler! |
| 33 | $state->serializeChildren( $node ); |
| 34 | return $node->nextSibling; |
| 35 | } |
| 36 | |
| 37 | /** @inheritDoc */ |
| 38 | public function before( Element $node, Node $otherNode, SerializerState $state ): array { |
| 39 | $otherNodeName = DOMCompat::nodeName( $otherNode ); |
| 40 | $tableCellOrBody = [ 'td', 'th', 'body' ]; |
| 41 | if ( $node->parentNode === $otherNode |
| 42 | && ( DOMUtils::isListItem( $otherNode ) || in_array( $otherNodeName, $tableCellOrBody, true ) ) |
| 43 | ) { |
| 44 | if ( in_array( $otherNodeName, $tableCellOrBody, true ) ) { |
| 45 | return [ 'min' => 0, 'max' => 1 ]; |
| 46 | } else { |
| 47 | return [ 'min' => 0, 'max' => 0 ]; |
| 48 | } |
| 49 | } elseif ( ( $otherNode === DiffDOMUtils::previousNonDeletedSibling( $node ) |
| 50 | // p-p transition |
| 51 | && $otherNode instanceof Element // for static analyzers |
| 52 | && $otherNodeName === 'p' |
| 53 | && ( DOMDataUtils::getDataParsoid( $otherNode )->stx ?? null ) !== 'html' ) |
| 54 | || ( self::treatAsPPTransition( $otherNode ) |
| 55 | && $otherNode === DiffDOMUtils::previousNonSepSibling( $node ) |
| 56 | // A new wikitext line could start at this P-tag. We have to figure out |
| 57 | // if 'node' needs a separation of 2 newlines from that P-tag. Examine |
| 58 | // previous siblings of 'node' to see if we emitted a block tag |
| 59 | // there => we can make do with 1 newline separator instead of 2 |
| 60 | // before the P-tag. |
| 61 | && !$this->currWikitextLineHasBlockNode( $state->currLine, $otherNode ) ) |
| 62 | || ( WTUtils::isMarkerAnnotation( DiffDOMUtils::nextNonSepSibling( $otherNode ) ) |
| 63 | && DiffDOMUtils::nextNonSepSibling( DiffDOMUtils::nextNonSepSibling( $otherNode ) ) === $node ) |
| 64 | ) { |
| 65 | return [ 'min' => 2, 'max' => 2 ]; |
| 66 | } elseif ( self::treatAsPPTransition( $otherNode ) |
| 67 | || ( DOMUtils::isWikitextBlockNode( $otherNode ) |
| 68 | && DOMCompat::nodeName( $otherNode ) !== 'blockquote' |
| 69 | && $node->parentNode === $otherNode ) |
| 70 | // new p-node added after sol-transparent wikitext should always |
| 71 | // get serialized onto a new wikitext line. |
| 72 | || ( WTUtils::emitsSolTransparentSingleLineWT( $otherNode ) |
| 73 | && WTUtils::isNewElt( $node ) ) |
| 74 | ) { |
| 75 | if ( !DOMUtils::hasNameOrHasAncestorOfName( $otherNode, 'figcaption' ) ) { |
| 76 | return [ 'min' => 1, 'max' => 2 ]; |
| 77 | } else { |
| 78 | return [ 'min' => 0, 'max' => 2 ]; |
| 79 | } |
| 80 | } else { |
| 81 | return [ 'min' => 0, 'max' => 2 ]; |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | /** @inheritDoc */ |
| 86 | public function after( Element $node, Node $otherNode, SerializerState $state ): array { |
| 87 | if ( !( $node->lastChild && DOMCompat::nodeName( $node->lastChild ) === 'br' ) |
| 88 | && self::isPPTransition( $otherNode ) |
| 89 | // A new wikitext line could start at this P-tag. We have to figure out |
| 90 | // if 'node' needs a separation of 2 newlines from that P-tag. Examine |
| 91 | // previous siblings of 'node' to see if we emitted a block tag |
| 92 | // there => we can make do with 1 newline separator instead of 2 |
| 93 | // before the P-tag. |
| 94 | && !$this->currWikitextLineHasBlockNode( $state->currLine, $node, true ) |
| 95 | // Since we are going to emit newlines before the other P-tag, we know it |
| 96 | // is going to start a new wikitext line. We have to figure out if 'node' |
| 97 | // needs a separation of 2 newlines from that P-tag. Examine following |
| 98 | // siblings of 'node' to see if we might emit a block tag there => we can |
| 99 | // make do with 1 newline separator instead of 2 before the P-tag. |
| 100 | && !$this->newWikitextLineMightHaveBlockNode( $otherNode ) |
| 101 | ) { |
| 102 | return [ 'min' => 2, 'max' => 2 ]; |
| 103 | } elseif ( DOMUtils::atTheTop( $otherNode ) ) { |
| 104 | return [ 'min' => 0, 'max' => 2 ]; |
| 105 | } elseif ( self::treatAsPPTransition( $otherNode ) |
| 106 | || ( DOMUtils::isWikitextBlockNode( $otherNode ) |
| 107 | && DOMCompat::nodeName( $otherNode ) !== 'blockquote' |
| 108 | && $node->parentNode === $otherNode ) |
| 109 | ) { |
| 110 | if ( !DOMUtils::hasNameOrHasAncestorOfName( $otherNode, 'figcaption' ) ) { |
| 111 | return [ 'min' => 1, 'max' => 2 ]; |
| 112 | } else { |
| 113 | return [ 'min' => 0, 'max' => 2 ]; |
| 114 | } |
| 115 | } else { |
| 116 | return [ 'min' => 0, 'max' => 2 ]; |
| 117 | } |
| 118 | } |
| 119 | |
| 120 | // IMPORTANT: Do not start walking from line.firstNode forward. Always |
| 121 | // walk backward from node. This is because in selser mode, it looks like |
| 122 | // line.firstNode doesn't always correspond to the wikitext line that is |
| 123 | // being processed since the previous emitted node might have been an unmodified |
| 124 | // DOM node that generated multiple wikitext lines. |
| 125 | |
| 126 | /** |
| 127 | * @param ?stdClass $line See SerializerState::$currLine |
| 128 | * @param Node $node |
| 129 | * @param bool $skipNode |
| 130 | * @return bool |
| 131 | */ |
| 132 | private function currWikitextLineHasBlockNode( |
| 133 | ?stdClass $line, Node $node, bool $skipNode = false |
| 134 | ): bool { |
| 135 | $parentNode = $node->parentNode; |
| 136 | if ( !$skipNode ) { |
| 137 | // If this node could break this wikitext line and emit |
| 138 | // non-ws content on a new line, the P-tag will be on that new line |
| 139 | // with text content that needs P-wrapping. |
| 140 | if ( preg_match( '/\n\S/', $node->textContent ) ) { |
| 141 | return false; |
| 142 | } |
| 143 | } |
| 144 | $node = DiffDOMUtils::previousNonDeletedSibling( $node ); |
| 145 | while ( !$node || !DOMUtils::atTheTop( $node ) ) { |
| 146 | while ( $node ) { |
| 147 | // If we hit a block node that will render on the same line, we are done! |
| 148 | if ( WTUtils::isBlockNodeWithVisibleWT( $node ) ) { |
| 149 | return true; |
| 150 | } |
| 151 | |
| 152 | // If this node could break this wikitext line, we are done. |
| 153 | // This is conservative because textContent could be looking at descendents |
| 154 | // of 'node' that may not have been serialized yet. But this is safe. |
| 155 | if ( str_contains( $node->textContent, "\n" ) ) { |
| 156 | return false; |
| 157 | } |
| 158 | |
| 159 | $node = DiffDOMUtils::previousNonDeletedSibling( $node ); |
| 160 | |
| 161 | // Don't go past the current line in any case. |
| 162 | if ( !empty( $line->firstNode ) && $node && |
| 163 | DOMUtils::isAncestorOf( $node, $line->firstNode ) |
| 164 | ) { |
| 165 | return false; |
| 166 | } |
| 167 | } |
| 168 | $node = $parentNode; |
| 169 | $parentNode = $node->parentNode; |
| 170 | } |
| 171 | |
| 172 | return false; |
| 173 | } |
| 174 | |
| 175 | private function newWikitextLineMightHaveBlockNode( Node $node ): bool { |
| 176 | $node = DiffDOMUtils::nextNonDeletedSibling( $node ); |
| 177 | while ( $node ) { |
| 178 | if ( $node instanceof Text ) { |
| 179 | // If this node will break this wikitext line, we are done! |
| 180 | if ( preg_match( '/\n/', $node->nodeValue ) ) { |
| 181 | return false; |
| 182 | } |
| 183 | } elseif ( $node instanceof Element ) { |
| 184 | // These tags will always serialize onto a new line |
| 185 | if ( |
| 186 | isset( Consts::$HTMLTagsRequiringSOLContext[DOMCompat::nodeName( $node )] ) && |
| 187 | !WTUtils::isLiteralHTMLNode( $node ) |
| 188 | ) { |
| 189 | return false; |
| 190 | } |
| 191 | |
| 192 | // We hit a block node that will render on the same line |
| 193 | if ( WTUtils::isBlockNodeWithVisibleWT( $node ) ) { |
| 194 | return true; |
| 195 | } |
| 196 | |
| 197 | // Go conservative |
| 198 | return false; |
| 199 | } |
| 200 | |
| 201 | $node = DiffDOMUtils::nextNonDeletedSibling( $node ); |
| 202 | } |
| 203 | return false; |
| 204 | } |
| 205 | |
| 206 | /** |
| 207 | * Node is being serialized before/after a P-tag. |
| 208 | * While computing newline constraints, this function tests |
| 209 | * if node should be treated as a P-wrapped node. |
| 210 | * @param Node $node |
| 211 | * @return bool |
| 212 | */ |
| 213 | private static function treatAsPPTransition( Node $node ): bool { |
| 214 | // Treat text/p similar to p/p transition |
| 215 | // If an element, it should not be a: |
| 216 | // * block node or literal HTML node |
| 217 | // * template wrapper |
| 218 | // * mw:Includes or Annotation meta or a SOL-transparent link |
| 219 | return $node instanceof Text |
| 220 | || ( !DOMUtils::atTheTop( $node ) |
| 221 | && !DOMUtils::isWikitextBlockNode( $node ) |
| 222 | && !WTUtils::isLiteralHTMLNode( $node ) |
| 223 | && !WTUtils::isEncapsulationWrapper( $node ) |
| 224 | && !WTUtils::isSolTransparentLink( $node ) |
| 225 | && !DOMUtils::matchTypeOf( $node, '#^mw:Includes/#' ) |
| 226 | && !DOMUtils::matchTypeOf( $node, '#^mw:Annotation/#' ) ); |
| 227 | } |
| 228 | |
| 229 | /** |
| 230 | * Test if $node is a P-wrapped node or should be treated as one. |
| 231 | * |
| 232 | * @param ?Node $node |
| 233 | * @return bool |
| 234 | */ |
| 235 | public static function isPPTransition( ?Node $node ): bool { |
| 236 | if ( !$node ) { |
| 237 | return false; |
| 238 | } |
| 239 | return ( $node instanceof Element // for static analyzers |
| 240 | && DOMCompat::nodeName( $node ) === 'p' |
| 241 | && ( DOMDataUtils::getDataParsoid( $node )->stx ?? '' ) !== 'html' ) |
| 242 | || self::treatAsPPTransition( $node ); |
| 243 | } |
| 244 | |
| 245 | } |