1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* Copyright (c) |
4
|
|
|
* - 2006-2013, Ivan Sagalaev (maniacsoftwaremaniacs.org), highlight.js |
5
|
|
|
* (original author) |
6
|
|
|
* - 2013-2019, Geert Bergman (geertscrivo.nl), highlight.php |
7
|
|
|
* - 2014 Daniel Lynge, highlight.php (contributor) |
8
|
|
|
* |
9
|
|
|
* Redistribution and use in source and binary forms, with or without |
10
|
|
|
* modification, are permitted provided that the following conditions are met: |
11
|
|
|
* |
12
|
|
|
* 1. Redistributions of source code must retain the above copyright notice, |
13
|
|
|
* this list of conditions and the following disclaimer. |
14
|
|
|
* 2. Redistributions in binary form must reproduce the above copyright notice, |
15
|
|
|
* this list of conditions and the following disclaimer in the documentation |
16
|
|
|
* and/or other materials provided with the distribution. |
17
|
|
|
* 3. Neither the name of "highlight.js", "highlight.php", nor the names of its |
18
|
|
|
* contributors may be used to endorse or promote products derived from this |
19
|
|
|
* software without specific prior written permission. |
20
|
|
|
* |
21
|
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
22
|
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
23
|
|
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
24
|
|
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE |
25
|
|
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
26
|
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
27
|
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
28
|
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
29
|
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
30
|
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
31
|
|
|
* POSSIBILITY OF SUCH DAMAGE. |
32
|
|
|
*/ |
33
|
|
|
|
34
|
|
|
namespace Highlight; |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* @todo In highlight.php 10.x, replace the @final attribute with the `final` keyword. |
38
|
|
|
* |
39
|
|
|
* @final |
40
|
|
|
* |
41
|
|
|
* @internal |
42
|
|
|
* |
43
|
|
|
* // Backward compatibility properties |
44
|
|
|
* |
45
|
|
|
* @property Mode $mode (DEPRECATED) All properties traditionally inside of $mode are now available directly from this class. |
46
|
|
|
* @property bool $caseInsensitive (DEPRECATED) Due to compatibility requirements with highlight.js, use `case_insensitive` instead. |
47
|
|
|
*/ |
48
|
|
|
class Language extends Mode |
49
|
|
|
{ |
50
|
|
|
/** @var string[] */ |
51
|
|
|
private static $COMMON_KEYWORDS = array('of', 'and', 'for', 'in', 'not', 'or', 'if', 'then'); |
52
|
|
|
|
53
|
|
|
/** @var string */ |
54
|
|
|
public $name; |
55
|
|
|
|
56
|
|
|
/** @var Mode|null */ |
57
|
|
|
private $mode = null; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* @param string $lang |
61
|
|
|
* @param string $filePath |
62
|
|
|
* |
63
|
|
|
* @throws \InvalidArgumentException when the given $filePath is inaccessible |
64
|
|
|
*/ |
65
|
|
|
public function __construct($lang, $filePath) |
66
|
|
|
{ |
67
|
|
|
$this->name = $lang; |
68
|
|
|
|
69
|
|
|
// We're loading the JSON definition file as an \stdClass object instead of an associative array. This is being |
70
|
|
|
// done to take advantage of objects being pass by reference automatically in PHP whereas arrays are pass by |
71
|
|
|
// value. |
72
|
|
|
$json = file_get_contents($filePath); |
73
|
|
|
|
74
|
|
|
if ($json === false) { |
75
|
|
|
throw new \InvalidArgumentException("Language file inaccessible: $filePath"); |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
$this->mode = json_decode($json); |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
/** |
82
|
|
|
* @param string $name |
83
|
|
|
* |
84
|
|
|
* @return bool|Mode|null |
85
|
|
|
*/ |
86
|
|
|
public function __get($name) |
87
|
|
|
{ |
88
|
|
|
if ($name === 'mode') { |
89
|
|
|
@trigger_error('The "mode" property will be removed in highlight.php 10.x', E_USER_DEPRECATED); |
90
|
|
|
|
91
|
|
|
return $this->mode; |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
if ($name === 'caseInsensitive') { |
95
|
|
|
@trigger_error('Due to compatibility requirements with highlight.js, use "case_insensitive" instead.', E_USER_DEPRECATED); |
96
|
|
|
|
97
|
|
|
if (isset($this->mode->case_insensitive)) { |
98
|
|
|
return $this->mode->case_insensitive; |
99
|
|
|
} |
100
|
|
|
|
101
|
|
|
return false; |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
if (isset($this->mode->{$name})) { |
105
|
|
|
return $this->mode->{$name}; |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
return null; |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* @param string $value |
113
|
|
|
* @param bool $global |
114
|
|
|
* |
115
|
|
|
* @return RegEx |
116
|
|
|
*/ |
117
|
|
|
private function langRe($value, $global = false) |
118
|
|
|
{ |
119
|
|
|
return RegExUtils::langRe($value, $global, $this->case_insensitive); |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
/** |
123
|
|
|
* Performs a shallow merge of multiple objects into one. |
124
|
|
|
* |
125
|
|
|
* @param Mode $params the objects to merge |
126
|
|
|
* @param array<string, mixed> ...$_ |
127
|
|
|
* |
128
|
|
|
* @return Mode |
129
|
|
|
*/ |
130
|
|
|
private function inherit($params, $_ = array()) |
131
|
|
|
{ |
132
|
|
|
/** @var Mode $result */ |
133
|
|
|
$result = new \stdClass(); |
134
|
|
|
$objects = func_get_args(); |
135
|
|
|
$parent = array_shift($objects); |
136
|
|
|
|
137
|
|
|
foreach ($parent as $key => $value) { |
138
|
|
|
$result->{$key} = $value; |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
foreach ($objects as $object) { |
142
|
|
|
foreach ($object as $key => $value) { |
143
|
|
|
$result->{$key} = $value; |
144
|
|
|
} |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
return $result; |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
/** |
151
|
|
|
* @param Mode|null $mode |
152
|
|
|
* |
153
|
|
|
* @return bool |
154
|
|
|
*/ |
155
|
|
|
private function dependencyOnParent($mode) |
156
|
|
|
{ |
157
|
|
|
if (!$mode) { |
158
|
|
|
return false; |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
if (isset($mode->endsWithParent) && $mode->endsWithParent) { |
162
|
|
|
return $mode->endsWithParent; |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
return $this->dependencyOnParent(isset($mode->starts) ? $mode->starts : null); |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
/** |
169
|
|
|
* @param Mode $mode |
170
|
|
|
* |
171
|
|
|
* @return array<int, \stdClass|Mode> |
172
|
|
|
*/ |
173
|
|
|
private function expandOrCloneMode($mode) |
174
|
|
|
{ |
175
|
|
|
if ($mode->variants && !$mode->cachedVariants) { |
|
|
|
|
176
|
|
|
$mode->cachedVariants = array(); |
177
|
|
|
|
178
|
|
|
foreach ($mode->variants as $variant) { |
179
|
|
|
$mode->cachedVariants[] = $this->inherit($mode, array('variants' => null), $variant); |
180
|
|
|
} |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
// EXPAND |
184
|
|
|
// if we have variants then essentially "replace" the mode with the variants |
185
|
|
|
// this happens in compileMode, where this function is called from |
186
|
|
|
if ($mode->cachedVariants) { |
|
|
|
|
187
|
|
|
return $mode->cachedVariants; |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
// CLONE |
191
|
|
|
// if we have dependencies on parents then we need a unique |
192
|
|
|
// instance of ourselves, so we can be reused with many |
193
|
|
|
// different parents without issue |
194
|
|
|
if ($this->dependencyOnParent($mode)) { |
195
|
|
|
return array($this->inherit($mode, array( |
196
|
|
|
'starts' => $mode->starts ? $this->inherit($mode->starts) : null, |
197
|
|
|
))); |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
// highlight.php does not have a concept freezing our Modes |
201
|
|
|
|
202
|
|
|
// no special dependency issues, just return ourselves |
203
|
|
|
return array($mode); |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
/** |
207
|
|
|
* @param Mode $mode |
208
|
|
|
* @param Mode|null $parent |
209
|
|
|
* |
210
|
|
|
* @return void |
211
|
|
|
*/ |
212
|
|
|
private function compileMode($mode, $parent = null) |
213
|
|
|
{ |
214
|
|
|
Mode::_normalize($mode); |
215
|
|
|
|
216
|
|
|
if ($mode->compiled) { |
217
|
|
|
return; |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
$mode->compiled = true; |
221
|
|
|
$mode->keywords = $mode->keywords ? $mode->keywords : $mode->beginKeywords; |
222
|
|
|
|
223
|
|
|
if ($mode->keywords) { |
224
|
|
|
$mode->keywords = $this->compileKeywords($mode->keywords, (bool) $this->case_insensitive); |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
$mode->lexemesRe = $this->langRe($mode->lexemes ? $mode->lexemes : "\w+", true); |
228
|
|
|
|
229
|
|
|
if ($parent) { |
230
|
|
|
if ($mode->beginKeywords) { |
231
|
|
|
$mode->begin = "\\b(" . implode("|", explode(" ", $mode->beginKeywords)) . ")\\b"; |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
if (!$mode->begin) { |
235
|
|
|
$mode->begin = "\B|\b"; |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
$mode->beginRe = $this->langRe($mode->begin); |
239
|
|
|
|
240
|
|
|
if ($mode->endSameAsBegin) { |
241
|
|
|
$mode->end = $mode->begin; |
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
if (!$mode->end && !$mode->endsWithParent) { |
245
|
|
|
$mode->end = "\B|\b"; |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
if ($mode->end) { |
249
|
|
|
$mode->endRe = $this->langRe($mode->end); |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
$mode->terminator_end = $mode->end; |
253
|
|
|
|
254
|
|
|
if ($mode->endsWithParent && $parent->terminator_end) { |
255
|
|
|
$mode->terminator_end .= ($mode->end ? "|" : "") . $parent->terminator_end; |
256
|
|
|
} |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
if ($mode->illegal) { |
260
|
|
|
$mode->illegalRe = $this->langRe($mode->illegal); |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
if ($mode->relevance === null) { |
264
|
|
|
$mode->relevance = 1; |
265
|
|
|
} |
266
|
|
|
|
267
|
|
|
if (!$mode->contains) { |
268
|
|
|
$mode->contains = array(); |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
/** @var Mode[] $expandedContains */ |
272
|
|
|
$expandedContains = array(); |
273
|
|
|
foreach ($mode->contains as &$c) { |
274
|
|
|
if ($c instanceof \stdClass) { |
275
|
|
|
Mode::_normalize($c); |
276
|
|
|
} |
277
|
|
|
|
278
|
|
|
$expandedContains = array_merge($expandedContains, $this->expandOrCloneMode( |
279
|
|
|
$c === 'self' ? $mode : $c |
280
|
|
|
)); |
281
|
|
|
} |
282
|
|
|
$mode->contains = $expandedContains; |
283
|
|
|
|
284
|
|
|
/** @var Mode $contain */ |
285
|
|
|
foreach ($mode->contains as $contain) { |
286
|
|
|
$this->compileMode($contain, $mode); |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
if ($mode->starts) { |
290
|
|
|
$this->compileMode($mode->starts, $parent); |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
$terminators = new Terminators($this->case_insensitive); |
294
|
|
|
$mode->terminators = $terminators->_buildModeRegex($mode); |
295
|
|
|
|
296
|
|
|
Mode::_handleDeprecations($mode); |
297
|
|
|
} |
298
|
|
|
|
299
|
|
|
/** |
300
|
|
|
* @param array<string, string>|string $rawKeywords |
301
|
|
|
* @param bool $caseSensitive |
302
|
|
|
* |
303
|
|
|
* @return array<string, array<int, string|int>> |
304
|
|
|
*/ |
305
|
|
|
private function compileKeywords($rawKeywords, $caseSensitive) |
306
|
|
|
{ |
307
|
|
|
/** @var array<string, array<int, string|int>> $compiledKeywords */ |
308
|
|
|
$compiledKeywords = array(); |
309
|
|
|
|
310
|
|
|
if (is_string($rawKeywords)) { |
311
|
|
|
$this->splitAndCompile("keyword", $rawKeywords, $compiledKeywords, $caseSensitive); |
312
|
|
|
} else { |
313
|
|
|
foreach ($rawKeywords as $className => $rawKeyword) { |
314
|
|
|
$this->splitAndCompile($className, $rawKeyword, $compiledKeywords, $caseSensitive); |
315
|
|
|
} |
316
|
|
|
} |
317
|
|
|
|
318
|
|
|
return $compiledKeywords; |
319
|
|
|
} |
320
|
|
|
|
321
|
|
|
/** |
322
|
|
|
* @param string $className |
323
|
|
|
* @param string $str |
324
|
|
|
* @param array<string, array<int, string|int>> $compiledKeywords |
325
|
|
|
* @param bool $caseSensitive |
326
|
|
|
* |
327
|
|
|
* @return void |
328
|
|
|
*/ |
329
|
|
|
private function splitAndCompile($className, $str, array &$compiledKeywords, $caseSensitive) |
330
|
|
|
{ |
331
|
|
|
if ($caseSensitive) { |
332
|
|
|
$str = strtolower($str); |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
$keywords = explode(' ', $str); |
336
|
|
|
|
337
|
|
|
foreach ($keywords as $keyword) { |
338
|
|
|
$pair = explode('|', $keyword); |
339
|
|
|
$providedScore = isset($pair[1]) ? $pair[1] : null; |
340
|
|
|
$compiledKeywords[$pair[0]] = array($className, $this->scoreForKeyword($pair[0], $providedScore)); |
341
|
|
|
} |
342
|
|
|
} |
343
|
|
|
|
344
|
|
|
/** |
345
|
|
|
* @param string $keyword |
346
|
|
|
* @param string $providedScore |
347
|
|
|
* |
348
|
|
|
* @return int |
349
|
|
|
*/ |
350
|
|
|
private function scoreForKeyword($keyword, $providedScore) |
351
|
|
|
{ |
352
|
|
|
if ($providedScore) { |
353
|
|
|
return (int) $providedScore; |
354
|
|
|
} |
355
|
|
|
|
356
|
|
|
return $this->commonKeyword($keyword) ? 0 : 1; |
357
|
|
|
} |
358
|
|
|
|
359
|
|
|
/** |
360
|
|
|
* @param string $word |
361
|
|
|
* |
362
|
|
|
* @return bool |
363
|
|
|
*/ |
364
|
|
|
private function commonKeyword($word) |
365
|
|
|
{ |
366
|
|
|
return in_array(strtolower($word), self::$COMMON_KEYWORDS); |
367
|
|
|
} |
368
|
|
|
|
369
|
|
|
/** |
370
|
|
|
* Compile the Language definition. |
371
|
|
|
* |
372
|
|
|
* @param bool $safeMode |
373
|
|
|
* |
374
|
|
|
* @since 9.17.1.0 The 'safeMode' parameter was added. |
375
|
|
|
* |
376
|
|
|
* @return void |
377
|
|
|
*/ |
378
|
|
|
public function compile($safeMode) |
379
|
|
|
{ |
380
|
|
|
if ($this->compiled) { |
381
|
|
|
return; |
382
|
|
|
} |
383
|
|
|
|
384
|
|
|
$jr = new JsonRef(); |
385
|
|
|
$jr->decodeRef($this->mode); |
386
|
|
|
|
387
|
|
|
// self is not valid at the top-level |
388
|
|
|
if (isset($this->mode->contains) && !in_array("self", $this->mode->contains)) { |
389
|
|
|
if (!$safeMode) { |
390
|
|
|
throw new \LogicException("`self` is not supported at the top-level of a language."); |
391
|
|
|
} |
392
|
|
|
$this->mode->contains = array_filter($this->mode->contains, function ($mode) { |
393
|
|
|
return $mode !== "self"; |
394
|
|
|
}); |
395
|
|
|
} |
396
|
|
|
|
397
|
|
|
$this->compileMode($this->mode); |
398
|
|
|
} |
399
|
|
|
|
400
|
|
|
/** |
401
|
|
|
* @todo Remove in highlight.php 10.x |
402
|
|
|
* |
403
|
|
|
* @deprecated 9.16.0 This method should never have been exposed publicly as part of the API. |
404
|
|
|
* |
405
|
|
|
* @param \stdClass|null $e |
406
|
|
|
* |
407
|
|
|
* @return void |
408
|
|
|
*/ |
409
|
|
|
public function complete(&$e) |
410
|
|
|
{ |
411
|
|
|
Mode::_normalize($e); |
412
|
|
|
} |
413
|
|
|
} |
414
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.