aboutsummaryrefslogtreecommitdiffstats
path: root/includes/skins/components/SkinComponentLink.php
blob: 57d42651bfcf99eab2f85c04a96e55cd113ee877 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
<?php
/**
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 * http://www.gnu.org/copyleft/gpl.html
 */

namespace MediaWiki\Skin;

use MediaWiki\Html\Html;
use MediaWiki\Linker\Linker;
use MediaWiki\Message\Message;
use MessageLocalizer;

/**
 * @internal for use inside Skin and SkinTemplate classes only
 * @unstable
 */
class SkinComponentLink implements SkinComponent {
	/** @var string */
	private $key;
	/** @var array */
	private $item;
	/** @var array */
	private $options;
	/** @var MessageLocalizer */
	private $localizer;

	/**
	 * @param string $key
	 * @param array $item
	 * @param MessageLocalizer $localizer
	 * @param array $options
	 */
	public function __construct( string $key, array $item, MessageLocalizer $localizer, array $options = [] ) {
		$this->key = $key;
		$this->item = $item;
		$this->localizer = $localizer;
		$this->options = $options;
	}

	private function msg( string $key ): Message {
		return $this->localizer->msg( $key );
	}

	/**
	 * Makes a link, usually used by makeListItem to generate a link for an item
	 * in a list used in navigation lists, portlets, portals, sidebars, etc...
	 *
	 * @param string $key Usually a key from the list you are generating this
	 * link from.
	 * @param array $item Contains some of a specific set of keys.
	 *
	 * The text of the link will be generated either from the contents of the
	 * "text" key in the $item array, if a "msg" key is present a message by
	 * that name will be used, and if neither of those are set the $key will be
	 * used as a message name. Escaping is handled by this method.
	 *
	 * If a "href" key is not present makeLink will just output htmlescaped text.
	 * The "href", "id", "class", "rel", and "type" keys are used as attributes
	 * for the link if present.
	 *
	 * If an "id" or "single-id" (if you don't want the actual id to be output
	 * on the link) is present it will be used to generate a tooltip and
	 * accesskey for the link.
	 *
	 * The 'link-html' key can be used to prepend additional HTML inside the link HTML.
	 * For example to prepend an icon.
	 *
	 * The keys "context" and "primary" are ignored; these keys are used
	 * internally by skins and are not supposed to be included in the HTML
	 * output.
	 *
	 * If you don't want an accesskey, set $item['tooltiponly'] = true;
	 *
	 * If a "data" key is present, it must be an array, where the keys represent
	 * the data-xxx properties with their provided values. For example,
	 *     $item['data'] = [
	 *       'foo' => 1,
	 *       'bar' => 'baz',
	 *     ];
	 * will render as element properties:
	 *     data-foo='1' data-bar='baz'
	 *
	 * The "class" key currently accepts both a string and an array of classes, but this will be
	 * changed to only accept an array in the future.
	 *
	 * @param array $options Can be used to affect the output of a link.
	 * Possible options are:
	 *   - 'class-as-property' key to specify that class attribute should be
	 *     not be included in array-attributes.
	 *   - 'text-wrapper' key to specify a list of elements to wrap the text of
	 *   a link in. This should be an array of arrays containing a 'tag' and
	 *   optionally an 'attributes' key. If you only have one element you don't
	 *   need to wrap it in another array. eg: To use <a><span>...</span></a>
	 *   in all links use [ 'text-wrapper' => [ 'tag' => 'span' ] ]
	 *   for your options.
	 *   - 'link-class' key can be used to specify additional classes to apply
	 *   to all links.
	 *   - 'link-fallback' can be used to specify a tag to use instead of "<a>"
	 *   if there is no link. eg: If you specify 'link-fallback' => 'span' than
	 *   any non-link will output a "<span>" instead of just text.
	 *
	 * @return array Associated array with the following keys:
	 * - html: HTML string
	 * - array-attributes: HTML attributes as array of objects:
	 * 		- key: Attribute name ex: 'href', 'class', 'id', ...
	 * 		- value: Attribute value
	 * 		NOTE: if options['class-as-property'] is set, class will not be included in the list.
	 * - text: Text of the link
	 * - class: Class of the link
	 */
	private function makeLink( $key, $item, $options = [] ) {
		$html = $item['html'] ?? null;
		$icon = $item['icon'] ?? null;
		if ( $html ) {
			return [
				'html' => $html
			];
		}
		$text = $item['text'] ?? $this->msg( $item['msg'] ?? $key )->text();

		$html = htmlspecialchars( $text );
		$isLink = isset( $item['href'] ) || isset( $options['link-fallback'] );

		if ( $html !== '' && isset( $options['text-wrapper'] ) ) {
			$wrapper = $options['text-wrapper'];
			if ( isset( $wrapper['tag'] ) ) {
				$wrapper = [ $wrapper ];
			}
			while ( count( $wrapper ) > 0 ) {
				$element = array_pop( $wrapper );
				'@phan-var array $element';

				$attrs = $element['attributes'] ?? [];
				// Apply title attribute to the outermost wrapper if there is
				// no link wrapper. No need for an accesskey.
				if ( count( $wrapper ) === 0 && !$isLink ) {
					$this->applyLinkTitleAttribs(
						$item,
						false,
						$attrs
					);
				}
				$html = Html::rawElement( $element['tag'], $attrs, $html );
			}
		}

		$attrs = [];
		$linkHtmlAttributes = [];
		$classAsProperty = $options['class-as-property'] ?? false;
		if ( $isLink ) {
			$attrs = $item;
			foreach ( [
				'single-id', 'text', 'msg', 'tooltiponly', 'context', 'primary',
				// These fields provide context for skins to modify classes.
				// They should not be outputted to skin.
				'icon', 'button',
				'tooltip-params', 'exists', 'link-html' ] as $k
			) {
				unset( $attrs[$k] );
			}

			if ( isset( $attrs['data'] ) ) {
				foreach ( $attrs['data'] as $key => $value ) {
					if ( $value === null ) {
						continue;
					}
					$attrs[ 'data-' . $key ] = $value;
				}
				unset( $attrs[ 'data' ] );
			}
			$this->applyLinkTitleAttribs( $item, true, $attrs );
			$class = $attrs['class'] ?? [];
			if ( isset( $options['link-class'] ) ) {
				$class = SkinComponentUtils::addClassToClassList(
					$class, $options['link-class']
				);
			}
			$attrs['class'] = is_array( $class ) ? implode( ' ', $class ) : $class;
			foreach ( $attrs as $key => $value ) {
				if ( $value === null ) {
					continue;
				}
				if ( $classAsProperty && $key === 'class' ) {
					continue;
				}
				$linkHtmlAttributes[] = [ 'key' => $key, 'value' => $value ];
			}

			if ( isset( $item['link-html'] ) ) {
				$html = $item['link-html'] . ' ' . $html;
			}

			$html = Html::rawElement( isset( $attrs['href'] )
				? 'a'
				: $options['link-fallback'], $attrs, $html );
		}
		$data = [
			'html' => $html,
			'icon' => $icon,
			'array-attributes' => count( $linkHtmlAttributes ) > 0 ? $linkHtmlAttributes : null,
			'text' => trim( $text ),
		];
		if ( $classAsProperty ) {
			$data['class'] = $attrs['class'] ?? '';
		}
		return $data;
	}

	/**
	 * Helper for makeLink(). Add tooltip and accesskey attributes to $attrs
	 * according to the input item array.
	 *
	 * @param array $item
	 * @param bool $allowAccessKey
	 * @param array &$attrs
	 */
	private function applyLinkTitleAttribs( $item, $allowAccessKey, &$attrs ) {
		$tooltipId = $item['single-id'] ?? $item['id'] ?? null;
		if ( $tooltipId === null ) {
			return;
		}
		$tooltipParams = $item['tooltip-params'] ?? [];
		$tooltipOption = isset( $item['exists'] ) && $item['exists'] === false ? 'nonexisting' : null;

		if ( !$allowAccessKey || !empty( $item['tooltiponly'] ) ) {
			$title = Linker::titleAttrib( $tooltipId, $tooltipOption, $tooltipParams );
			if ( $title !== false ) {
				$attrs['title'] = $title;
			}
		} else {
			$tip = Linker::tooltipAndAccesskeyAttribs(
				$tooltipId,
				$tooltipParams,
				$tooltipOption,
				$this->localizer
			);
			if ( isset( $tip['title'] ) && $tip['title'] !== false ) {
				$attrs['title'] = $tip['title'];
			}
			if ( isset( $tip['accesskey'] ) && $tip['accesskey'] !== false ) {
				$attrs['accesskey'] = $tip['accesskey'];
			}
		}
	}

	/**
	 * @inheritDoc
	 */
	public function getTemplateData(): array {
		return $this->makeLink( $this->key, $this->item, $this->options );
	}
}