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