Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 62
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 / 62
0.00% covered (danger)
0.00%
0 / 5
380
0.00% covered (danger)
0.00%
0 / 1
 getTextNodeDSRStart
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
56
 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\DOM\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        if ( !$parent instanceof Element ) {
29            // This will be a DocumentFragment while processing embedded fragments
30            // during the combined DOMPP pass that processes them.
31            return null;
32        }
33        $dsr = DOMDataUtils::getDataParsoid( $parent )->dsr ?? null;
34        if ( !Utils::isValidDSR( $dsr, true ) ) {
35            return null;
36        }
37        $start = $dsr->innerStart();
38        $c = $parent->firstChild;
39        while ( $c !== $node ) {
40            if ( $c instanceof Comment ) {
41                $start += WTUtils::decodedCommentLength( $c );
42            } elseif ( $c instanceof Text ) {
43                $start += strlen( $c->nodeValue );
44            } else {
45                '@phan-var Element $c';  /** @var Element $c */
46                $dsr = DOMDataUtils::getDataParsoid( $c )->dsr ?? null;
47                if ( !Utils::isValidDSR( $dsr ) ) {
48                    return null;
49                }
50                $start = $dsr->end;
51            }
52            $c = $c->nextSibling;
53        }
54        return $start;
55    }
56
57    private static function insertDisplaySpace(
58        Text $node, int $offset
59    ): void {
60        $str = $node->nodeValue;
61
62        $prefix = substr( $str, 0, $offset );
63        $suffix = substr( $str, $offset + 1 );
64
65        $node->nodeValue = $prefix;
66
67        $doc = $node->ownerDocument;
68        $post = $doc->createTextNode( $suffix );
69        $node->parentNode->insertBefore( $post, $node->nextSibling );
70
71        $start = self::getTextNodeDSRStart( $node );
72        if ( $start !== null ) {
73            $start += strlen( $prefix );
74            $dsr = new DomSourceRange( $start, $start + 1, 0, 0 );
75        } else {
76            $dsr = new DomSourceRange( null, null, null, null );
77        }
78
79        $span = $doc->createElement( 'span' );
80        $span->appendChild( $doc->createTextNode( "\u{00A0}" ) );
81        $span->setAttribute( 'typeof', 'mw:DisplaySpace' );
82        $dp = new DataParsoid;
83        $dp->dsr = $dsr;
84        DOMDataUtils::setDataParsoid( $span, $dp );
85        $node->parentNode->insertBefore( $span, $post );
86    }
87
88    /**
89     * Omit handling node
90     *
91     * @param Node $node
92     * @return bool|Node
93     */
94    private static function omitNode( Node $node ) {
95        $nodeName = DOMCompat::nodeName( $node );
96
97        // Go to next sibling if we encounter pre or raw text elements
98        if ( $nodeName === 'pre' || DOMUtils::isRawTextElement( $node ) ) {
99            return $node->nextSibling;
100        }
101
102        // Run handlers only on text nodes
103        if ( !( $node instanceof Text ) ) {
104            return true;
105        }
106
107        return false;
108    }
109
110    /**
111     * French spaces, Guillemet-left
112     *
113     * @param Node $node
114     * @return bool|Element
115     */
116    public static function leftHandler( Node $node ) {
117        $omit = self::omitNode( $node );
118        if ( $omit !== false ) {
119            return $omit;
120        }
121
122        '@phan-var Text $node'; // @var Text $node
123
124        $key = array_keys( array_slice( Sanitizer::FIXTAGS, 0, 1 ) )[0];
125        if ( preg_match( $key, $node->nodeValue, $matches, PREG_OFFSET_CAPTURE ) ) {
126            $offset = $matches[0][1];
127            self::insertDisplaySpace( $node, $offset );
128            return true;
129        }
130        return true;
131    }
132
133    /**
134     * French spaces, Guillemet-right
135     *
136     * @param Node $node
137     * @return bool|Element
138     */
139    public static function rightHandler( Node $node ) {
140        $omit = self::omitNode( $node );
141        if ( $omit !== false ) {
142            return $omit;
143        }
144
145        '@phan-var Text $node'; // @var Text $node
146
147        $key = array_keys( array_slice( Sanitizer::FIXTAGS, 1, 1 ) )[0];
148        if ( preg_match( $key, $node->nodeValue, $matches, PREG_OFFSET_CAPTURE ) ) {
149            $offset = $matches[1][1] + strlen( $matches[1][0] );
150            self::insertDisplaySpace( $node, $offset );
151            return true;
152        }
153        return true;
154    }
155
156}