Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 146
LanguageVariantHandler
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 5
3660
0.00% covered (danger)
0.00%
0 / 146
 __construct
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 convertOne
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 11
 compressSpArray
0.00% covered (danger)
0.00%
0 / 1
72
0.00% covered (danger)
0.00%
0 / 14
 onLanguageVariant
0.00% covered (danger)
0.00%
0 / 1
2256
0.00% covered (danger)
0.00%
0 / 118
 onTag
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 1
<?php
declare( strict_types = 1 );
namespace Wikimedia\Parsoid\Wt2Html\TT;
use Wikimedia\Parsoid\NodeData\DataParsoid;
use Wikimedia\Parsoid\Tokens\EndTagTk;
use Wikimedia\Parsoid\Tokens\EOFTk;
use Wikimedia\Parsoid\Tokens\KV;
use Wikimedia\Parsoid\Tokens\SourceRange;
use Wikimedia\Parsoid\Tokens\TagTk;
use Wikimedia\Parsoid\Tokens\Token;
use Wikimedia\Parsoid\Utils\ContentUtils;
use Wikimedia\Parsoid\Utils\DOMUtils;
use Wikimedia\Parsoid\Utils\PHPUtils;
use Wikimedia\Parsoid\Utils\PipelineUtils;
use Wikimedia\Parsoid\Wikitext\Consts;
use Wikimedia\Parsoid\Wt2Html\TokenTransformManager;
/**
 * Handler for language conversion markup, which looks like `-{ ... }-`.
 */
class LanguageVariantHandler extends TokenHandler {
    /** @inheritDoc */
    public function __construct( TokenTransformManager $manager, array $options ) {
        parent::__construct( $manager, $options );
    }
    /**
     * convert one variant text to dom.
     * @param TokenTransformManager $manager
     * @param array $options
     * @param string $t
     * @param array $attribs
     * @return array
     */
    private function convertOne( TokenTransformManager $manager, array $options, string $t,
        array $attribs ): array {
        // we're going to fetch the actual token list from attribs
        // (this ensures that it has gone through the earlier stages
        // of the pipeline already to be expanded)
        $t = PHPUtils::stripPrefix( $t, 'mw:lv' );
        $srcOffsets = $attribs[$t]->srcOffsets;
        $domFragment = PipelineUtils::processContentInPipeline(
            $this->env, $manager->getFrame(), array_merge( $attribs[$t]->v, [ new EOFTk() ] ),
            [
                'pipelineType' => 'tokens/x-mediawiki/expanded',
                'pipelineOpts' => [
                    'inlineContext' => true,
                    'expandTemplates' => $options['expandTemplates'],
                    'inTemplate' => $options['inTemplate']
                ],
                'srcOffsets' => $srcOffsets->value ?? null,
                'sol' => true
            ]
        );
        return [
            'xmlstr' => ContentUtils::ppToXML(
                $domFragment, [ 'innerXML' => true ]
            ),
            'isBlock' => DOMUtils::hasBlockElementDescendant( $domFragment ),
        ];
    }
    /**
     * compress a whitespace sequence
     * @param ?array $a
     * @return ?array
     */
    private function compressSpArray( ?array $a ): ?array {
        $result = [];
        $ctr = 0;
        if ( $a === null ) {
            return $a;
        }
        foreach ( $a as $sp ) {
            if ( $sp === '' ) {
                $ctr++;
            } else {
                if ( $ctr > 0 ) {
                    $result[] = $ctr;
                    $ctr = 0;
                }
                $result[] = $sp;
            }
        }
        if ( $ctr > 0 ) {
            $result[] = $ctr;
        }
        return $result;
    }
    /**
     * Main handler.
     * See {@link TokenTransformManager#addTransform}'s transformation parameter
     * @param Token $token
     * @return TokenHandlerResult|null
     */
    private function onLanguageVariant( Token $token ): ?TokenHandlerResult {
        $manager = $this->manager;
        $options = $this->options;
        $attribs = $token->attribs;
        $dataAttribs = $token->dataAttribs;
        $tsr = $dataAttribs->tsr;
        $flags = $dataAttribs->flags;
        $flagSp = $dataAttribs->flagSp;
        $isMeta = false;
        $sawFlagA = false;
        // remove trailing semicolon marker, if present
        $trailingSemi = false;
        if ( count( $dataAttribs->texts ) &&
            ( $dataAttribs->texts[count( $dataAttribs->texts ) - 1]['semi'] ?? null )
        ) {
            $trailingSemi = array_pop( $dataAttribs->texts )['sp'] ?? null;
        }
        // convert all variant texts to DOM
        $isBlock = false;
        $texts = array_map( function ( array $t ) use ( $manager, $options, $attribs, &$isBlock ) {
            $text = null;
            $from = null;
            $to = null;
            if ( isset( $t['twoway'] ) ) {
                $text = $this->convertOne( $manager, $options, $t['text'], $attribs );
                $isBlock = $isBlock || !empty( $text['isBlock'] );
                return [ 'lang' => $t['lang'], 'text' => $text['xmlstr'], 'twoway' => true, 'sp' => $t['sp'] ];
            } elseif ( isset( $t['lang'] ) ) {
                $from = $this->convertOne( $manager, $options, $t['from'], $attribs );
                $to = $this->convertOne( $manager, $options, $t['to'], $attribs );
                $isBlock = $isBlock || !empty( $from['isBlock'] ) || !empty( $to['isBlock'] );
                return [ 'lang' => $t['lang'], 'from' => $from['xmlstr'], 'to' => $to['xmlstr'],
                    'sp' => $t['sp'] ];
            } else {
                $text = $this->convertOne( $manager, $options, $t['text'], $attribs );
                $isBlock = $isBlock || !empty( $text['isBlock'] );
                return [ 'text' => $text['xmlstr'], 'sp' => [] ];
            }
        }, $dataAttribs->texts );
        // collect two-way/one-way conversion rules
        $oneway = [];
        $twoway = [];
        $sawTwoway = false;
        $sawOneway = false;
        $textSp = null;
        $twowaySp = [];
        $onewaySp = [];
        foreach ( $texts as $t ) {
            if ( isset( $t['twoway'] ) ) {
                $twoway[] = [ 'l' => $t['lang'], 't' => $t['text'] ];
                array_push( $twowaySp, $t['sp'][0], $t['sp'][1], $t['sp'][2] );
                $sawTwoway = true;
            } elseif ( isset( $t['lang'] ) ) {
                $oneway[] = [ 'l' => $t['lang'], 'f' => $t['from'], 't' => $t['to'] ];
                array_push( $onewaySp, $t['sp'][0], $t['sp'][1], $t['sp'][2], $t['sp'][3] );
                $sawOneway = true;
            }
        }
        // To avoid too much data-mw bloat, only the top level keys in
        // data-mw-variant are "human readable".  Nested keys are single-letter:
        // `l` for `language`, `t` for `text` or `to`, `f` for `from`.
        $dataMWV = null;
        if ( count( $flags ) === 0 && count( $dataAttribs->variants ) > 0 ) {
            // "Restrict possible variants to a limited set"
            $dataMWV = [
                'filter' => [ 'l' => $dataAttribs->variants, 't' => $texts[0]['text'] ],
                'show' => true
            ];
        } else {
            $dataMWV = [];
            foreach ( $flags as $f ) {
                if ( array_key_exists( $f, Consts::$LCFlagMap ) ) {
                    if ( Consts::$LCFlagMap[$f] ) {
                        $dataMWV[Consts::$LCFlagMap[$f]] = true;
                        if ( $f === 'A' ) {
                            $sawFlagA = true;
                        }
                    }
                } else {
                    $dataMWV['error'] = true;
                }
            }
            // (this test is done at the top of ConverterRule::getRuleConvertedStr)
            // (also partially in ConverterRule::parse)
            if ( count( $texts ) === 1 &&
                !isset( $texts[0]['lang'] ) && !isset( $dataMWV['name'] )
            ) {
                if ( isset( $dataMWV['add'] ) || isset( $dataMWV['remove'] ) ) {
                    $variants = [ '*' ];
                    $twoway = array_map( static function ( string $code ) use ( $texts, &$sawTwoway ) {
                        return [ 'l' => $code, 't' => $texts[0]['text'] ];
                    }, $variants );
                    $sawTwoway = true;
                } else {
                    $dataMWV['disabled'] = true;
                    unset( $dataMWV['describe'] );
                }
            }
            if ( isset( $dataMWV['describe'] ) ) {
                if ( !$sawFlagA ) {
                    $dataMWV['show'] = true;
                }
            }
            if ( isset( $dataMWV['disabled'] ) || isset( $dataMWV['name'] ) ) {
                if ( isset( $dataMWV['disabled'] ) ) {
                    $dataMWV['disabled'] = [ 't' => $texts[0]['text'] ?? '' ];
                } else {
                    $dataMWV['name'] = [ 't' => $texts[0]['text'] ?? '' ];
                }
                if ( isset( $dataMWV['title'] ) || isset( $dataMWV['add'] ) ) {
                    unset( $dataMWV['show'] );
                } else {
                    $dataMWV['show'] = true;
                }
            } elseif ( $sawTwoway ) {
                $dataMWV['twoway'] = $twoway;
                $textSp = $twowaySp;
                if ( $sawOneway ) {
                    $dataMWV['error'] = true;
                }
            } else {
                $dataMWV['oneway'] = $oneway;
                $textSp = $onewaySp;
                if ( !$sawOneway ) {
                    $dataMWV['error'] = true;
                }
            }
        }
        // Use meta/not meta instead of explicit 'show' flag.
        $isMeta = !isset( $dataMWV['show'] );
        unset( $dataMWV['show'] );
        // Trim some data from data-parsoid if it matches the defaults
        if ( count( $flagSp ) === 2 * count( $dataAttribs->original ) ) {
            $result = true;
            foreach ( $flagSp as $s ) {
                if ( $s !== '' ) {
                    $result = false;
                    break;
                }
            }
            if ( $result ) {
                $flagSp = null;
            }
        }
        if ( $trailingSemi !== false && $textSp ) {
            $textSp[] = $trailingSemi;
        }
        // Our markup is always the same, except for the contents of
        // the data-mw-variant attribute and whether it's a span, div, or a
        // meta, depending on (respectively) whether conversion output
        // contains only inline content, could contain block content,
        // or never contains any content.
        $das = new DataParsoid;
        $das->fl = $dataAttribs->original; // original "fl"ags
        $flSp = $this->compressSpArray( $flagSp ); // spaces around flags
        if ( $flSp !== null ) {
            $das->flSp = $flSp;
        }
        $das->src = $dataAttribs->src;
        $tSp = $this->compressSpArray( $textSp ); // spaces around texts
        if ( $tSp !== null ) {
            $das->tSp = $tSp;
        }
        $das->tsr = new SourceRange( $tsr->start, $isMeta ? $tsr->end : ( $tsr->end - 2 ) );
        PHPUtils::sortArray( $dataMWV );
        $tokens = [
            new TagTk( $isMeta ? 'meta' : ( $isBlock ? 'div' : 'span' ), [
                    new KV( 'typeof', 'mw:LanguageVariant' ),
                    new KV( 'data-mw-variant', PHPUtils::jsonEncode( $dataMWV ) )
                ], $das
            )
        ];
        if ( !$isMeta ) {
            $metaDP = new DataParsoid;
            $metaDP->tsr = new SourceRange( $tsr->end - 2, $tsr->end );
            $tokens[] = new EndTagTk( $isBlock ? 'div' : 'span', [], $metaDP );
        }
        return new TokenHandlerResult( $tokens );
    }
    /**
     * @inheritDoc
     */
    public function onTag( Token $token ): ?TokenHandlerResult {
        return $token->getName() === 'language-variant' ? $this->onLanguageVariant( $token ) : null;
    }
}