Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 93
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
TipNodeRenderer
0.00% covered (danger)
0.00%
0 / 93
0.00% covered (danger)
0.00%
0 / 11
992
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setMessageLocalizer
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 render
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 buildHtml
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
72
 getBaseCssClasses
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 mainAndTextRender
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 getMessageKeyWithVariantFallback
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 graphicRender
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 getImageSourcePath
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 exampleRender
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 getMessageParameters
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
56
1<?php
2
3namespace GrowthExperiments\HelpPanel\Tips;
4
5use LogicException;
6use MediaWiki\Html\Html;
7use MediaWiki\Output\OutputPage;
8use MessageLocalizer;
9use OOUI\IconWidget;
10
11/**
12 * Transform an array of TipNodes into an array of rendered HTML.
13 */
14class TipNodeRenderer {
15    /**
16     * @var MessageLocalizer
17     */
18    private $messageLocalizer;
19
20    /**
21     * @var string
22     */
23    private $extensionAssetsPath;
24
25    /**
26     * @param string $extensionAssetsPath
27     */
28    public function __construct( string $extensionAssetsPath ) {
29        $this->extensionAssetsPath = $extensionAssetsPath;
30    }
31
32    /**
33     * @param MessageLocalizer $messageLocalizer
34     */
35    public function setMessageLocalizer( MessageLocalizer $messageLocalizer ) {
36        $this->messageLocalizer = $messageLocalizer;
37    }
38
39    /**
40     * Render a set of tip nodes into HTML.
41     *
42     * This method is called recursively as the TipNode tree is rendered.
43     *
44     * @param TipNode[] $nodes
45     * @param string $skinName
46     * @param string $dir
47     * @return array
48     *   An array of rendered HTML.
49     */
50    public function render( array $nodes, string $skinName, string $dir ): array {
51        OutputPage::setupOOUI( $skinName, $dir );
52        return array_values( array_map( function ( $node ) use ( $skinName, $dir ) {
53            return $this->buildHtml( $node, $skinName, $dir );
54        }, $nodes ) );
55    }
56
57    /**
58     * @param TipNode $node
59     * @param string $skin
60     * @param string $dir
61     * @return string
62     */
63    private function buildHtml( TipNode $node, string $skin, string $dir ): string {
64        switch ( $node->getType() ) {
65            case 'header':
66            case 'main':
67            case 'main-multiple':
68            case 'text':
69                return $this->mainAndTextRender( $node, $skin );
70            case 'graphic':
71                return $this->graphicRender( $node, $dir );
72            case 'example':
73                return $this->exampleRender( $node );
74            default:
75                throw new LogicException( $node->getType() . 'is not a valid tip type ID.' );
76        }
77    }
78
79    /**
80     * @param string $tipTypeId
81     * @param array $textVariants
82     * @return array|string[]
83     */
84    private function getBaseCssClasses( string $tipTypeId, array $textVariants = [] ): array {
85        $variants = array_map( static function ( $variant ) {
86            return 'growthexperiments-quickstart-tips-tip--' . $variant;
87        }, $textVariants );
88        return array_merge( [
89            'growthexperiments-quickstart-tips-tip',
90            'growthexperiments-quickstart-tips-tip-' . $tipTypeId
91        ], $variants );
92    }
93
94    /**
95     * @param TipNode $node
96     * @param string $skinName
97     * @return string
98     */
99    private function mainAndTextRender( TipNode $node, string $skinName ): string {
100        $tipTextVariants = array_values( array_map( static function ( $item ) {
101            if ( $item['type'] == TipTree::TIP_DATA_TYPE_TEXT_VARIANT ) {
102                return $item['data'];
103            }
104            return null;
105        }, $node->getData() ) );
106
107        return Html::rawElement( 'div', [
108            'class' => $this->getBaseCssClasses( $node->getType(), $tipTextVariants )
109        ], $this->messageLocalizer->msg(
110            $this->getMessageKeyWithVariantFallback( $node ), $this->getMessageParameters( $node, $skinName )
111        )->parse() );
112    }
113
114    /**
115     * Obtain a message key for use with Message.
116     *
117     * This is usually determined by TipLoader, which finds a i18n key based
118     * on the current editor, skin, task type and tip type. But this method
119     * allows for overriding with a variant in the event the TipNode specifies
120     * a title type but the value for that title is not present.
121     *
122     * @param TipNode $node
123     * @return string
124     */
125    private function getMessageKeyWithVariantFallback( TipNode $node ): string {
126        $messageKey = $node->getMessageKey();
127        $messageKeyVariant = current( array_filter( array_map( static function ( $nodeConfig ) {
128            // This could be more flexible, but as we don't have a use
129            // case yet, leaving as is for now.
130            if ( $nodeConfig['type'] === TipTree::TIP_DATA_TYPE_TITLE &&
131                !$nodeConfig['data']['title'] ) {
132                return $nodeConfig['data']['messageKeyVariant'] ?? [];
133            }
134            return [];
135        }, $node->getData() ) ) );
136        if ( $messageKeyVariant ) {
137            $messageKey .= $messageKeyVariant;
138        }
139        return $messageKey;
140    }
141
142    /**
143     * @param TipNode $node
144     * @param string $dir
145     * @return string
146     */
147    private function graphicRender( TipNode $node, string $dir ): string {
148        if ( !$node->getData()[0]['type'] || $node->getData()[0]['type'] !== 'image' ) {
149            return '';
150        }
151        return Html::rawElement( 'img', [
152            'class' => $this->getBaseCssClasses( $node->getType() ),
153            'src' => $this->getImageSourcePath(
154                $node->getData()[0]['data']['filename'],
155                $node->getData()[0]['data']['suffix'],
156                $dir
157            ),
158            // Leaving alt blank per T245786#6115403; screen readers
159            // should ignore this decorative image.
160            'alt' => '',
161        ] );
162    }
163
164    /**
165     * @param string $filename
166     * @param string $suffix
167     * @param string $dir
168     * @return string
169     */
170    private function getImageSourcePath( string $filename, string $suffix, string $dir ): string {
171        return $this->extensionAssetsPath . '/GrowthExperiments/images/' .
172            $filename . '-' . $dir . '.' . $suffix;
173    }
174
175    /**
176     * @param TipNode $node
177     * @return string
178     */
179    private function exampleRender( TipNode $node ): string {
180        $exampleLabelKey = $node->getData()[0]['data']['labelKey'] ?? null;
181        $exampleLabel = $exampleLabelKey ?
182            Html::element( 'div',
183            [ 'class' => 'growthexperiments-quickstart-tips-tip-example-label' ],
184                $this->messageLocalizer->msg( $exampleLabelKey )->text() )
185            : '';
186        return $exampleLabel . Html::rawElement( 'div', [
187            'class' => $this->getBaseCssClasses( $node->getType() )
188        ],
189            Html::rawElement( 'p', [
190            'class' => [
191                'growthexperiments-quickstart-tips-tip-' . $node->getType() . '-text'
192            ] ], $this->messageLocalizer->msg(
193                $this->getMessageKeyWithVariantFallback( $node )
194            )->parse() ) );
195    }
196
197    /**
198     * @param TipNode $node
199     * @param string $skinName
200     * @return array
201     */
202    private function getMessageParameters( TipNode $node, string $skinName ): array {
203        return array_filter( array_map( function ( $nodeConfig ) use ( $skinName ) {
204            switch ( $nodeConfig['type'] ) {
205                case TipTree::TIP_DATA_TYPE_PLAIN_MESSAGE:
206                    $parameterMessageKey = $nodeConfig['variant'][$skinName]['data'] ?? $nodeConfig['data'];
207                    return $this->messageLocalizer->msg( $parameterMessageKey )->plain();
208                case TipTree::TIP_DATA_TYPE_OOUI_ICON:
209                    $iconConfig = [
210                        'icon' => $nodeConfig['data']['icon'],
211                        'framed' => $nodeConfig['data']['framed'] ?? true
212                    ];
213                    if ( isset( $nodeConfig['data']['labelKey'] ) ) {
214                        $iconConfig['label'] = $this->messageLocalizer->msg(
215                            $nodeConfig['data']['labelKey']
216                        )->plain();
217                    }
218                    return new IconWidget( $iconConfig );
219                case TipTree::TIP_DATA_TYPE_TITLE:
220                    return $nodeConfig['data']['title'];
221                case TipTree::TIP_DATA_TYPE_TEXT_VARIANT:
222                    return null;
223                default:
224                    throw new LogicException( $nodeConfig['type'] . ' is not supported' );
225            }
226        }, $node->getData() ) );
227    }
228
229}