Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 60
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
DisplaySpace
0.00% covered (danger)
0.00%
0 / 60
0.00% covered (danger)
0.00%
0 / 5
342
0.00% covered (danger)
0.00%
0 / 1
 getTextNodeDSRStart
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
42
 insertDisplaySpace
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
6
 omitNode
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 leftHandler
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 rightHandler
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\Parsoid\Wt2Html\PP\Handlers;
5
6use Wikimedia\Parsoid\Core\DomSourceRange;
7use Wikimedia\Parsoid\Core\Sanitizer;
8use Wikimedia\Parsoid\DOM\Comment;
9use Wikimedia\Parsoid\DOM\Element;
10use Wikimedia\Parsoid\DOM\Node;
11use Wikimedia\Parsoid\DOM\Text;
12use Wikimedia\Parsoid\NodeData\DataParsoid;
13use Wikimedia\Parsoid\Utils\DOMCompat;
14use Wikimedia\Parsoid\Utils\DOMDataUtils;
15use Wikimedia\Parsoid\Utils\DOMUtils;
16use Wikimedia\Parsoid\Utils\Utils;
17use Wikimedia\Parsoid\Utils\WTUtils;
18
19/**
20 * Apply french space armoring.
21 *
22 * See https://www.mediawiki.org/wiki/Specs/HTML#Display_space
23 */
24class 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}