1 | <?php |
||
2 | |||
3 | /** |
||
4 | * File holding the Lingo\Element class. |
||
5 | * |
||
6 | * This file is part of the MediaWiki extension Lingo. |
||
7 | * |
||
8 | * @copyright 2011 - 2018, Stephan Gambke |
||
9 | * @license GPL-2.0-or-later |
||
10 | * |
||
11 | * The Lingo extension is free software: you can redistribute it and/or modify |
||
12 | * it under the terms of the GNU General Public License as published by the Free |
||
13 | * Software Foundation; either version 2 of the License, or (at your option) any |
||
14 | * later version. |
||
15 | * |
||
16 | * The Lingo extension is distributed in the hope that it will be useful, but |
||
17 | * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
||
18 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
||
19 | * details. |
||
20 | * |
||
21 | * You should have received a copy of the GNU General Public License along |
||
22 | * with this program. If not, see <http://www.gnu.org/licenses/>. |
||
23 | * |
||
24 | * @author Stephan Gambke |
||
25 | * |
||
26 | * @file |
||
27 | * @ingroup Lingo |
||
28 | */ |
||
29 | |||
30 | namespace Lingo; |
||
31 | |||
32 | use DOMDocument; |
||
33 | use DOMElement; |
||
34 | use DOMText; |
||
35 | use Title; |
||
36 | |||
37 | /** |
||
38 | * This class represents a term-definition pair. |
||
39 | * One term may be related to several definitions. |
||
40 | * |
||
41 | * @ingroup Lingo |
||
42 | */ |
||
43 | class Element { |
||
44 | const ELEMENT_TERM = 0; |
||
45 | const ELEMENT_DEFINITION = 1; |
||
46 | const ELEMENT_SOURCE = 2; |
||
47 | const ELEMENT_LINK = 3; |
||
48 | const ELEMENT_STYLE = 4; |
||
49 | |||
50 | const ELEMENT_FIELDCOUNT = 5; // number of fields stored for each element; (last field's index) + 1 |
||
51 | |||
52 | const LINK_TEMPLATE_ID = 'LingoLink'; |
||
53 | |||
54 | private $formattedTerm = null; |
||
55 | private $formattedDefinitions = null; |
||
56 | |||
57 | private $mDefinitions = []; |
||
58 | private $mTerm = null; |
||
59 | |||
60 | private $hasBeenDisplayed = false; |
||
61 | |||
62 | /** |
||
63 | * Lingo\Element constructor. |
||
64 | * |
||
65 | * @param string &$term |
||
66 | * @param string[] &$definition |
||
67 | */ |
||
68 | 12 | public function __construct( &$term, &$definition ) { |
|
69 | 12 | $this->mTerm = $term; |
|
70 | 12 | $this->addDefinition( $definition ); |
|
71 | 12 | } |
|
72 | |||
73 | /** |
||
74 | * @param array &$definition |
||
75 | */ |
||
76 | 11 | public function addDefinition( &$definition ) { |
|
77 | 11 | $this->mDefinitions[] = $definition + array_fill( 0, self::ELEMENT_FIELDCOUNT, null ); |
|
78 | 11 | } |
|
79 | |||
80 | /** |
||
81 | * @param DOMDocument &$doc |
||
82 | * |
||
83 | * @return DOMElement|DOMText |
||
84 | */ |
||
85 | 11 | public function getFormattedTerm( DOMDocument &$doc ) { |
|
86 | 11 | global $wgexLingoDisplayOnce; |
|
87 | |||
88 | 11 | if ( $wgexLingoDisplayOnce && $this->hasBeenDisplayed ) { |
|
89 | 2 | return $doc->createTextNode( $this->mTerm ); |
|
90 | } |
||
91 | |||
92 | 11 | $this->hasBeenDisplayed = true; |
|
93 | |||
94 | 11 | $this->buildFormattedTerm( $doc ); |
|
95 | |||
96 | 11 | return $this->formattedTerm->cloneNode( true ); |
|
97 | } |
||
98 | |||
99 | /** |
||
100 | * @param DOMDocument &$doc |
||
101 | */ |
||
102 | 11 | private function buildFormattedTerm( DOMDocument &$doc ) { |
|
103 | // only create if not yet created |
||
104 | 11 | if ( $this->formattedTerm === null || $this->formattedTerm->ownerDocument !== $doc ) { |
|
105 | |||
106 | 11 | if ( $this->isSimpleLink() ) { |
|
107 | 7 | $this->formattedTerm = $this->buildFormattedTermAsLink( $doc ); |
|
108 | } else { |
||
109 | 4 | $this->formattedTerm = $this->buildFormattedTermAsTooltip( $doc ); |
|
110 | } |
||
111 | } |
||
112 | 11 | } |
|
113 | |||
114 | /** |
||
115 | * @return bool |
||
116 | */ |
||
117 | 11 | private function isSimpleLink() { |
|
118 | 11 | return count( $this->mDefinitions ) === 1 && |
|
119 | 11 | !is_string( $this->mDefinitions[ 0 ][ self::ELEMENT_DEFINITION ] ) && |
|
120 | 11 | is_string( $this->mDefinitions[ 0 ][ self::ELEMENT_LINK ] ); |
|
121 | } |
||
122 | |||
123 | /** |
||
124 | * @param DOMDocument &$doc |
||
125 | * @return DOMElement |
||
126 | */ |
||
127 | 7 | protected function buildFormattedTermAsLink( DOMDocument &$doc ) { |
|
128 | 7 | $linkTarget = $this->mDefinitions[ 0 ][ self::ELEMENT_LINK ]; |
|
129 | 7 | $descriptor = $this->getDescriptorFromLinkTarget( $linkTarget ); |
|
130 | |||
131 | 7 | if ( $descriptor === null ) { |
|
132 | 2 | $this->mDefinitions = []; |
|
133 | 2 | $this->addErrorMessageForInvalidLink( $linkTarget ); |
|
134 | 2 | return $this->buildFormattedTermAsTooltip( $doc ); |
|
135 | } |
||
136 | |||
137 | // create link element |
||
138 | 5 | $link = $doc->createElement( 'a', htmlentities( $this->mDefinitions[ 0 ][ self::ELEMENT_TERM ] ) ); |
|
139 | |||
140 | // set the link target |
||
141 | 5 | $link->setAttribute( 'href', $descriptor[ 'url' ] ); |
|
142 | 5 | $link->setAttribute( 'class', implode( ' ', $this->getClassesForLink( $descriptor ) ) ); |
|
143 | |||
144 | 5 | $title = $this->getTitleForLink( $descriptor ); |
|
145 | 5 | if ( $title !== null ) { |
|
146 | 4 | $link->setAttribute( 'title', $title ); |
|
147 | } |
||
148 | |||
149 | 5 | return $link; |
|
150 | } |
||
151 | |||
152 | /** |
||
153 | * @param DOMDocument &$doc |
||
154 | * |
||
155 | * @return DOMElement |
||
156 | */ |
||
157 | 6 | protected function buildFormattedTermAsTooltip( DOMDocument &$doc ) { |
|
158 | // Wrap term and definition in <span> tags |
||
159 | 6 | $span = $doc->createElement( 'span', htmlentities( $this->mTerm ) ); |
|
160 | 6 | $span->setAttribute( 'class', 'mw-lingo-term' ); |
|
161 | 6 | $span->setAttribute( 'data-lingo-term-id', $this->getId() ); |
|
162 | |||
163 | 6 | return $span; |
|
164 | } |
||
165 | |||
166 | /** |
||
167 | * @param array $descriptor |
||
168 | * |
||
169 | * @return string[] |
||
170 | */ |
||
171 | 5 | protected function getClassesForLink( $descriptor ) { |
|
172 | // TODO: should this be more elaborate? |
||
173 | // Cleanest would probably be to use Linker::link and parse it |
||
174 | // back into a DOMElement, but we are in a somewhat time-critical |
||
175 | // part here. |
||
176 | |||
177 | // set style |
||
178 | 5 | $classes = [ 'mw-lingo-term' ]; |
|
179 | |||
180 | 5 | $classes[] = $this->mDefinitions[ 0 ][ self::ELEMENT_STYLE ]; |
|
181 | |||
182 | 5 | if ( array_key_exists( 'title', $descriptor ) && $descriptor[ 'title' ] instanceof Title ) { |
|
183 | |||
184 | 4 | if ( !$descriptor['title']->isKnown() ) { |
|
185 | 2 | $classes[] = 'new'; |
|
186 | } |
||
187 | |||
188 | 4 | if ( $descriptor['title']->isExternal() ) { |
|
189 | 4 | $classes[] = 'extiw'; |
|
190 | } |
||
191 | |||
192 | } else { |
||
193 | 1 | $classes[] = 'ext'; |
|
194 | } |
||
195 | |||
196 | 5 | return array_filter( $classes ); |
|
197 | } |
||
198 | |||
199 | /** |
||
200 | * @param array $descriptor |
||
201 | * @return string |
||
202 | */ |
||
203 | protected function getTitleForLink( $descriptor ) { |
||
204 | /** @var \Title $target */ |
||
205 | 5 | $target = $descriptor[ 'title' ]; |
|
206 | |||
207 | 5 | if ( is_string( $target ) ) { |
|
208 | return $target; |
||
209 | 5 | } |
|
210 | 1 | ||
211 | if ( $target->getPrefixedText() === '' ) { |
||
212 | return null; |
||
213 | 4 | } |
|
214 | 1 | ||
215 | if ( $target->isKnown() ) { |
||
216 | return $target->getPrefixedText(); |
||
217 | 3 | } |
|
218 | 1 | ||
219 | return wfMessage( 'red-link-title', $target->getPrefixedText() )->text(); |
||
220 | } |
||
221 | 2 | ||
222 | /** |
||
223 | * @return string[] |
||
224 | */ |
||
225 | public function getFormattedDefinitions() { |
||
226 | if ( $this->formattedDefinitions === null ) { |
||
227 | 4 | $this->buildFormattedDefinitions(); |
|
228 | 4 | } |
|
229 | 4 | ||
230 | return $this->formattedDefinitions; |
||
231 | } |
||
232 | 4 | ||
233 | /** |
||
234 | */ |
||
235 | protected function buildFormattedDefinitions() { |
||
236 | if ( $this->isSimpleLink() ) { |
||
237 | 4 | $this->formattedDefinitions = ''; |
|
238 | 4 | return; |
|
239 | 1 | } |
|
240 | 1 | ||
241 | $divDefinitions = "<div class='mw-lingo-tooltip' id='{$this->getId()}'>"; |
||
242 | |||
243 | 3 | $definition = reset( $this->mDefinitions ); |
|
244 | while ( $definition !== false ) { |
||
245 | 3 | ||
246 | 3 | $text = $definition[ self::ELEMENT_DEFINITION ]; |
|
247 | $link = $definition[ self::ELEMENT_LINK ]; |
||
248 | 3 | $style = $definition[ self::ELEMENT_STYLE ]; |
|
249 | 3 | ||
250 | 3 | // navigation-not-searchable removes definition from CirrusSearch index |
|
251 | $divDefinitions .= "<div class='mw-lingo-definition navigation-not-searchable {$style}'>" |
||
252 | 3 | . "<div class='mw-lingo-definition-text'>\n{$text}\n</div>"; |
|
253 | |||
254 | 3 | if ( $link !== null ) { |
|
255 | |||
256 | 2 | $descriptor = $this->getDescriptorFromLinkTarget( $link ); |
|
257 | |||
258 | 2 | if ( $descriptor === null ) { |
|
259 | 1 | $this->addErrorMessageForInvalidLink( $link ); |
|
260 | } else { |
||
261 | 1 | $divDefinitions .= "<div class='mw-lingo-definition-link'>[{$descriptor[ 'url' ]} <nowiki/>]</div>"; |
|
262 | } |
||
263 | } |
||
264 | |||
265 | 3 | $divDefinitions .= "</div>"; |
|
0 ignored issues
–
show
|
|||
266 | |||
267 | 3 | $definition = next( $this->mDefinitions ); |
|
268 | } |
||
269 | |||
270 | 3 | $divDefinitions .= "\n</div>"; |
|
271 | |||
272 | 3 | $this->formattedDefinitions = $divDefinitions; |
|
273 | 3 | } |
|
274 | |||
275 | /** |
||
276 | * @return string |
||
277 | */ |
||
278 | 6 | public function getId() { |
|
279 | 6 | return md5( $this->mTerm ); |
|
280 | } |
||
281 | |||
282 | /** |
||
283 | * @param string $linkTarget |
||
284 | * |
||
285 | * @return string[] |
||
286 | */ |
||
287 | 9 | protected function getDescriptorFromLinkTarget( $linkTarget ) { |
|
288 | 9 | if ( $this->isValidLinkTarget( $linkTarget ) ) { |
|
289 | 1 | return [ 'url' => $linkTarget, 'title' => $this->mTerm ]; |
|
290 | } |
||
291 | |||
292 | 8 | $title = Title::newFromText( $linkTarget ); |
|
293 | |||
294 | 8 | if ( $title !== null ) { |
|
295 | 5 | return [ 'url' => $title->getFullURL(), 'title' => $title ]; |
|
296 | } |
||
297 | |||
298 | 3 | return null; |
|
299 | } |
||
300 | |||
301 | /** |
||
302 | * @param string $linkTarget |
||
303 | * |
||
304 | * @return bool |
||
305 | */ |
||
306 | 9 | protected function isValidLinkTarget( $linkTarget ) { |
|
307 | 9 | return wfParseUrl( $linkTarget ) !== false; |
|
308 | } |
||
309 | |||
310 | /** |
||
311 | * @param string $link |
||
312 | */ |
||
313 | 3 | protected function addErrorMessageForInvalidLink( $link ) { |
|
314 | 3 | $errorMessage = wfMessage( 'lingo-invalidlinktarget', $this->mTerm, $link )->text(); |
|
315 | 3 | $errorDefinition = [ self::ELEMENT_DEFINITION => $errorMessage, self::ELEMENT_STYLE => 'invalid-link-target' ]; |
|
316 | |||
317 | 3 | $this->addDefinition( $errorDefinition ); |
|
318 | 3 | } |
|
319 | |||
320 | } |
||
321 |
PHP provides two ways to mark string literals. Either with single quotes
'literal'
or with double quotes"literal"
. The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (
\'
) and the backslash (\\
). Every other character is displayed as is.Double quoted string literals may contain other variables or more complex escape sequences.
will print an indented:
Single is Value
If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.
For more information on PHP string literals and available escape sequences see the PHP core documentation.