Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
CharInsert
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 8
156
0.00% covered (danger)
0.00%
0 / 1
 charInsertHook
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 expand
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 processLine
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 armor
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 processItem
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 insertChar
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 getInsertAttribute
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\CharInsert;
4
5use MediaWiki\Parser\Parser;
6use MediaWiki\Parser\Sanitizer;
7use MediaWiki\Xml\Xml;
8
9class CharInsert {
10    /** @var array XML-style attributes passed to the tag */
11    private $params;
12    /** @var Parser */
13    private $parser;
14
15    /**
16     * Main entry point, called by the parser.
17     * @param string|null $data The textual content of the <charinsert> tag
18     * @param array $params XML-style attributes of the <charinsert> tag
19     * @param Parser $parser
20     * @return string
21     */
22    public static function charInsertHook( $data, array $params, Parser $parser ): string {
23        if ( $data === null ) {
24            return '';
25        }
26
27        return ( new self( $params, $parser ) )->expand( $data );
28    }
29
30    /**
31     * Constructor.
32     * @param array $params XML-style attributes of the <charinsert> tag
33     * @param Parser $parser
34     */
35    private function __construct( array $params, Parser $parser ) {
36        $this->params = $params;
37        $this->parser = $parser;
38    }
39
40    /**
41     * Parse the content of a whole <charinsert> tag.
42     * @param string $data The textual content of the <charinsert> tag
43     * @return string HTML to be inserted in the parser output
44     */
45    private function expand( $data ): string {
46        $data = $this->parser->getStripState()->unstripBoth( $data );
47        $this->parser->getOutput()->addModules( [ 'ext.charinsert' ] );
48        $this->parser->getOutput()->addModuleStyles( [ 'ext.charinsert.styles' ] );
49        return implode( "<br />\n",
50            array_map( [ $this, 'processLine' ],
51                explode( "\n", trim( $data ) ) ) );
52    }
53
54    /**
55     * Parse a single line in the <charinsert> tag.
56     * @param string $data Textual content of the line
57     * @return string HTML to be inserted in the parser output
58     */
59    private function processLine( string $data ): string {
60        return implode( "\n",
61            array_map( [ $this, 'processItem' ],
62                preg_split( '/\s+/', $this->armor( $data ) ) ) );
63    }
64
65    /**
66     * Escape literal whitespace characters in <nowiki> tags. Whitespace
67     * within <nowiki> tags is not considered to be an insert boundary.
68     * @param string $data Textual content of the line to escape whitespace in
69     * @return string The textual content with whitespace escaped and <nowiki>
70     *  tags removed
71     */
72    private function armor( string $data ): string {
73        return preg_replace_callback(
74            '!<nowiki>(.*?)</nowiki>!i',
75            static function ( array $matches ) {
76                return strtr( $matches[1], [
77                    '\t' => '&#9;',
78                    '\r' => '&#12;',
79                    ' ' => '&#32;',
80                ] );
81            },
82            $data
83        );
84    }
85
86    /**
87     * Parse a single insert, i.e. largest portion of the input that
88     * (after going through armor()) doesn’t contain ASCII whitespace.
89     * @param string $data The single insert.
90     * @return string HTML to be inserted in the parser output
91     */
92    private function processItem( string $data ): string {
93        $chars = explode( '+', $data );
94        if ( count( $chars ) > 1 && $chars[0] !== '' ) {
95            return $this->insertChar( $chars[0], $chars[1] );
96        } elseif ( count( $chars ) === 1 ) {
97            return $this->insertChar( $chars[0] );
98        } else {
99            return $this->insertChar( '+' );
100        }
101    }
102
103    /**
104     * Create the HTML for a single insert.
105     * @param string $start The start part of the insert, inserted before
106     *  the text selected by the user (if there is any).
107     * @param string $end The end part of the insert, inserted after
108     *  the text selected by the user (if there is any).
109     * @return string HTML to be inserted in the parser output
110     */
111    private function insertChar( string $start, string $end = '' ): string {
112        $estart = $this->getInsertAttribute( $start );
113        $eend = $this->getInsertAttribute( $end );
114        $inline = $this->params['label'] ?? ( $estart . $eend );
115
116        // Having no href attribute makes the link be more
117        // easily copy and pasteable for non-js users.
118        return Xml::element( 'a',
119            [
120                'data-mw-charinsert-start' => $estart,
121                'data-mw-charinsert-end' => $eend,
122                'class' => 'mw-charinsert-item'
123            ], $inline
124        );
125    }
126
127    /**
128     * Double-escape non-breaking spaces. This allows the user to
129     * differentiate the user normal and non-breaking spaces.
130     * @param string $text The text to double-escape NBSPs in
131     * @return string
132     */
133    private function getInsertAttribute( string $text ): string {
134        static $invisibles = [ '&nbsp;', '&#160;' ];
135        static $visibles = [ '&amp;nbsp;', '&amp;#160;' ];
136        return Sanitizer::decodeCharReferences(
137            str_replace( $invisibles, $visibles, $text ) );
138    }
139}