Language.php 11 KB

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