Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 46
0.00% covered (danger)
0.00%
0 / 1
CRAP
0.00% covered (danger)
0.00%
0 / 1
Poem
0.00% covered (danger)
0.00%
0 / 46
0.00% covered (danger)
0.00%
0 / 1
90
0.00% covered (danger)
0.00%
0 / 1
 sourceToDom
0.00% covered (danger)
0.00%
0 / 46
0.00% covered (danger)
0.00%
0 / 1
90
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Poem\Parsoid;
5
6use Wikimedia\Parsoid\DOM\DocumentFragment;
7use Wikimedia\Parsoid\Ext\ExtensionTagHandler;
8use Wikimedia\Parsoid\Ext\ParsoidExtensionAPI;
9use Wikimedia\Parsoid\Ext\PHPUtils;
10use Wikimedia\Parsoid\Utils\DOMCompat;
11
12class Poem extends ExtensionTagHandler {
13    /** @inheritDoc */
14    public function sourceToDom(
15        ParsoidExtensionAPI $extApi, string $content, array $extArgs
16    ): DocumentFragment {
17        /*
18         * Transform wikitext found in <poem>...</poem>
19         * 1. Strip leading & trailing newlines
20         * 2. Suppress indent-pre by replacing leading space with &nbsp;
21         * 3. Replace colons with <span class='...' style='...'>...</span>
22         * 4. Add <br/> for newlines except (a) in nowikis (b) after ----
23         */
24
25        if ( strlen( $content ) > 0 ) {
26            // 1. above
27            $content = PHPUtils::stripPrefix( $content, "\n" );
28            $content = PHPUtils::stripSuffix( $content, "\n" );
29
30            // 2. above
31            $content = preg_replace( '/^ /m', '&nbsp;', $content );
32
33            // 3. above
34            $contentArray = explode( "\n", $content );
35            $contentMap = array_map( static function ( $line ) use ( $extApi ) {
36                $i = 0;
37                $lineLength = strlen( $line );
38                while ( $i < $lineLength && $line[$i] === ':' ) {
39                    $i++;
40                }
41                if ( $i > 0 && $i < $lineLength ) {
42                    $domFragment = $extApi->htmlToDom( '' );
43                    $doc = $domFragment->ownerDocument;
44                    $span = $doc->createElement( 'span' );
45                    $span->setAttribute( 'class', 'mw-poem-indented' );
46                    $span->setAttribute( 'style', 'display: inline-block; margin-inline-start: ' . $i . 'em;' );
47                    // $line isn't an HTML text node, it's wikitext that will be passed to extTagToDOM
48                    return substr( DOMCompat::getOuterHTML( $span ), 0, -7 ) .
49                        ltrim( $line, ':' ) . '</span>';
50                } else {
51                    return $line;
52                }
53            }, $contentArray );
54            // TODO: Use faster? preg_replace
55            $content = implode( "\n", $contentMap );
56
57            // 4. above
58            // Split on <nowiki>..</nowiki> fragments.
59            // Process newlines inside nowikis in a post-processing pass.
60            // If <br/>s are added here, Parsoid will escape them to plaintext.
61            $splitContent = preg_split( '/(<nowiki>[\s\S]*?<\/nowiki>)/', $content,
62                -1, PREG_SPLIT_DELIM_CAPTURE );
63            $content = implode( '',
64                array_map( static function ( $p, $i ) {
65                    if ( $i % 2 === 1 ) {
66                        return $p;
67                    }
68
69                    // This is a hack that exploits the fact that </poem>
70                    // cannot show up in the extension's content.
71                    return preg_replace( '/^(-+)<\/poem>/m', "\$1\n",
72                        preg_replace( '/\n/m', "<br/>\n",
73                            preg_replace( '/(^----+)\n/m', '$1</poem>', $p ) ) );
74                },
75                $splitContent,
76                range( 0, count( $splitContent ) - 1 ) )
77            );
78
79        }
80
81        // Add the 'poem' class to the 'class' attribute, or if not found, add it
82        $value = $extApi->findAndUpdateArg( $extArgs, 'class', static function ( string $value ) {
83            return strlen( $value ) ? "poem {$value}" : 'poem';
84        } );
85
86        if ( !$value ) {
87            $extApi->addNewArg( $extArgs, 'class', 'poem' );
88        }
89
90        return $extApi->extTagToDOM( $extArgs, $content, [
91                'wrapperTag' => 'div',
92                'parseOpts' => [ 'extTag' => 'poem' ],
93                // Create new frame, because $content doesn't literally appear in
94                // the parent frame's sourceText (our copy has been munged)
95                'processInNewFrame' => true,
96                // We've shifted the content around quite a bit when we preprocessed
97                // it.  In the future if we wanted to enable selser inside the <poem>
98                // body we should create a proper offset map and then apply it to the
99                // result after the parse, like we do in the Gallery extension.
100                // But for now, since we don't selser the contents, just strip the
101                // DSR info so it doesn't cause problems/confusion with unicode
102                // offset conversion (and so it's clear you can't selser what we're
103                // currently emitting).
104                'clearDSROffsets' => true
105            ]
106        );
107    }
108}