Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 60 |
|
0.00% |
0 / 5 |
CRAP | |
0.00% |
0 / 1 |
DisplaySpace | |
0.00% |
0 / 60 |
|
0.00% |
0 / 5 |
342 | |
0.00% |
0 / 1 |
getTextNodeDSRStart | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
42 | |||
insertDisplaySpace | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
6 | |||
omitNode | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 | |||
leftHandler | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
rightHandler | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace Wikimedia\Parsoid\Wt2Html\PP\Handlers; |
5 | |
6 | use Wikimedia\Parsoid\Core\DomSourceRange; |
7 | use Wikimedia\Parsoid\Core\Sanitizer; |
8 | use Wikimedia\Parsoid\DOM\Comment; |
9 | use Wikimedia\Parsoid\DOM\Element; |
10 | use Wikimedia\Parsoid\DOM\Node; |
11 | use Wikimedia\Parsoid\DOM\Text; |
12 | use Wikimedia\Parsoid\NodeData\DataParsoid; |
13 | use Wikimedia\Parsoid\Utils\DOMCompat; |
14 | use Wikimedia\Parsoid\Utils\DOMDataUtils; |
15 | use Wikimedia\Parsoid\Utils\DOMUtils; |
16 | use Wikimedia\Parsoid\Utils\Utils; |
17 | use Wikimedia\Parsoid\Utils\WTUtils; |
18 | |
19 | /** |
20 | * Apply french space armoring. |
21 | * |
22 | * See https://www.mediawiki.org/wiki/Specs/HTML#Display_space |
23 | */ |
24 | class DisplaySpace { |
25 | |
26 | private static function getTextNodeDSRStart( Text $node ): ?int { |
27 | $parent = $node->parentNode; |
28 | '@phan-var Element $parent'; /** @var Element $parent */ |
29 | $dsr = DOMDataUtils::getDataParsoid( $parent )->dsr ?? null; |
30 | if ( !Utils::isValidDSR( $dsr, true ) ) { |
31 | return null; |
32 | } |
33 | $start = $dsr->innerStart(); |
34 | $c = $parent->firstChild; |
35 | while ( $c !== $node ) { |
36 | if ( $c instanceof Comment ) { |
37 | $start += WTUtils::decodedCommentLength( $c ); |
38 | } elseif ( $c instanceof Text ) { |
39 | $start += strlen( $c->nodeValue ); |
40 | } else { |
41 | '@phan-var Element $c'; /** @var Element $c */ |
42 | $dsr = DOMDataUtils::getDataParsoid( $c )->dsr ?? null; |
43 | if ( !Utils::isValidDSR( $dsr ) ) { |
44 | return null; |
45 | } |
46 | $start = $dsr->end; |
47 | } |
48 | $c = $c->nextSibling; |
49 | } |
50 | return $start; |
51 | } |
52 | |
53 | private static function insertDisplaySpace( |
54 | Text $node, int $offset |
55 | ): void { |
56 | $str = $node->nodeValue; |
57 | |
58 | $prefix = substr( $str, 0, $offset ); |
59 | $suffix = substr( $str, $offset + 1 ); |
60 | |
61 | $node->nodeValue = $prefix; |
62 | |
63 | $doc = $node->ownerDocument; |
64 | $post = $doc->createTextNode( $suffix ); |
65 | $node->parentNode->insertBefore( $post, $node->nextSibling ); |
66 | |
67 | $start = self::getTextNodeDSRStart( $node ); |
68 | if ( $start !== null ) { |
69 | $start += strlen( $prefix ); |
70 | $dsr = new DomSourceRange( $start, $start + 1, 0, 0 ); |
71 | } else { |
72 | $dsr = new DomSourceRange( null, null, null, null ); |
73 | } |
74 | |
75 | $span = $doc->createElement( 'span' ); |
76 | $span->appendChild( $doc->createTextNode( "\u{00A0}" ) ); |
77 | $span->setAttribute( 'typeof', 'mw:DisplaySpace' ); |
78 | $dp = new DataParsoid; |
79 | $dp->dsr = $dsr; |
80 | DOMDataUtils::setDataParsoid( $span, $dp ); |
81 | $node->parentNode->insertBefore( $span, $post ); |
82 | } |
83 | |
84 | /** |
85 | * Omit handling node |
86 | * |
87 | * @param Node $node |
88 | * @return bool|Node |
89 | */ |
90 | private static function omitNode( Node $node ) { |
91 | $nodeName = DOMCompat::nodeName( $node ); |
92 | |
93 | // Go to next sibling if we encounter pre or raw text elements |
94 | if ( $nodeName === 'pre' || DOMUtils::isRawTextElement( $node ) ) { |
95 | return $node->nextSibling; |
96 | } |
97 | |
98 | // Run handlers only on text nodes |
99 | if ( !( $node instanceof Text ) ) { |
100 | return true; |
101 | } |
102 | |
103 | return false; |
104 | } |
105 | |
106 | /** |
107 | * French spaces, Guillemet-left |
108 | * |
109 | * @param Node $node |
110 | * @return bool|Element |
111 | */ |
112 | public static function leftHandler( Node $node ) { |
113 | $omit = self::omitNode( $node ); |
114 | if ( $omit !== false ) { |
115 | return $omit; |
116 | } |
117 | |
118 | '@phan-var Text $node'; // @var Text $node |
119 | |
120 | $key = array_keys( array_slice( Sanitizer::FIXTAGS, 0, 1 ) )[0]; |
121 | if ( preg_match( $key, $node->nodeValue, $matches, PREG_OFFSET_CAPTURE ) ) { |
122 | $offset = $matches[0][1]; |
123 | self::insertDisplaySpace( $node, $offset ); |
124 | return true; |
125 | } |
126 | return true; |
127 | } |
128 | |
129 | /** |
130 | * French spaces, Guillemet-right |
131 | * |
132 | * @param Node $node |
133 | * @return bool|Element |
134 | */ |
135 | public static function rightHandler( Node $node ) { |
136 | $omit = self::omitNode( $node ); |
137 | if ( $omit !== false ) { |
138 | return $omit; |
139 | } |
140 | |
141 | '@phan-var Text $node'; // @var Text $node |
142 | |
143 | $key = array_keys( array_slice( Sanitizer::FIXTAGS, 1, 1 ) )[0]; |
144 | if ( preg_match( $key, $node->nodeValue, $matches, PREG_OFFSET_CAPTURE ) ) { |
145 | $offset = $matches[1][1] + strlen( $matches[1][0] ); |
146 | self::insertDisplaySpace( $node, $offset ); |
147 | return true; |
148 | } |
149 | return true; |
150 | } |
151 | |
152 | } |