SluggedBehavior.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  1. <?php
  2. namespace Tools\Model\Behavior;
  3. use ArrayObject;
  4. use Cake\Core\Configure;
  5. use Cake\Datasource\EntityInterface;
  6. use Cake\Event\EventInterface;
  7. use Cake\ORM\Behavior;
  8. use Cake\ORM\Query;
  9. use Cake\ORM\Table;
  10. use Cake\Utility\Inflector;
  11. use InvalidArgumentException;
  12. use RuntimeException;
  13. use Shim\Utility\Inflector as ShimInflector;
  14. /**
  15. * SluggedBehavior
  16. * Part based/inspired by the sluggable behavior of Mariano Iglesias
  17. *
  18. * Usage: See docs
  19. *
  20. * @author Andy Dawson
  21. * @author Mark Scherer
  22. * @license MIT
  23. */
  24. class SluggedBehavior extends Behavior {
  25. /**
  26. * @var string
  27. */
  28. public const MODE_URL = 'url';
  29. /**
  30. * @var string
  31. */
  32. public const MODE_ASCII = 'ascii';
  33. /**
  34. * Default config
  35. *
  36. * - label:
  37. * set to the name of a field to use for the slug, an array of fields to use as slugs or leave as null to rely
  38. * on the format returned by find('list') to determine the string to use for slugs
  39. * - field: The slug field name
  40. * - overwriteField: The boolean field to trigger overwriting if "overwrite" is false
  41. * - mode: has the following values
  42. * ascii - returns an ascii slug generated using the core Inflector::slug() function
  43. * display - a dummy mode which returns a slug legal for display - removes illegal (not unprintable) characters
  44. * url - returns a slug appropriate to put in a URL
  45. * class - a dummy mode which returns a slug appropriate to put in a html class (there are no restrictions)
  46. * id - returns a slug appropriate to use in a HTML id
  47. * OR pass it a callable as custom method to be invoked
  48. * - separator: The separator to use
  49. * - length:
  50. * Set to 0 for no length. Will be auto-detected if possible via schema.
  51. * - overwrite: has 2 values
  52. * false - once the slug has been saved, do not change it (use if you are doing lookups based on slugs)
  53. * true - if the label field values change, regenerate the slug (use if you are the slug is just window-dressing)
  54. * - unique: has 2 values
  55. * false - will not enforce a unique slug, whatever the label is is directly slugged without checking for duplicates
  56. * true - use if you are doing lookups based on slugs (see overwrite)
  57. * - case: has the following values
  58. * null - don't change the case of the slug
  59. * low - force lower case. E.g. "this-is-the-slug"
  60. * up - force upper case E.g. "THIS-IS-THE-SLUG"
  61. * title - force title case. E.g. "This-Is-The-Slug"
  62. * camel - force CamelCase. E.g. "ThisIsTheSlug"
  63. * - replace: custom replacements as array
  64. * - on: beforeSave or beforeRules
  65. * - scope: certain conditions to use as scope
  66. * - tidy: If cleanup should be run on slugging
  67. *
  68. * @var array<string, mixed>
  69. */
  70. protected $_defaultConfig = [
  71. 'label' => null,
  72. 'field' => 'slug',
  73. 'overwriteField' => 'overwrite_slug',
  74. 'mode' => 'url',
  75. 'separator' => '-',
  76. 'length' => null,
  77. 'overwrite' => false,
  78. 'unique' => false,
  79. 'notices' => true,
  80. 'case' => null,
  81. 'replace' => [
  82. '&' => 'and',
  83. '+' => 'and',
  84. '#' => 'hash',
  85. ],
  86. 'on' => 'beforeRules',
  87. 'scope' => [],
  88. 'tidy' => true,
  89. 'implementedFinders' => [
  90. 'slugged' => 'findSlugged',
  91. ],
  92. 'implementedMethods' => [
  93. 'slug' => 'slug',
  94. 'generateSlug' => 'generateSlug',
  95. 'resetSlugs' => 'resetSlugs',
  96. 'needsSlugUpdate' => 'needsSlugUpdate',
  97. ],
  98. ];
  99. /**
  100. * Table instance
  101. *
  102. * @var \Cake\ORM\Table
  103. */
  104. protected $_table;
  105. /**
  106. * @param \Cake\ORM\Table $table
  107. * @param array $config
  108. */
  109. public function __construct(Table $table, array $config = []) {
  110. $this->_defaultConfig['notices'] = Configure::read('debug');
  111. foreach ($this->_defaultConfig['replace'] as $key => $value) {
  112. $this->_defaultConfig['replace'][$key] = __d('tools', $value);
  113. }
  114. $config += (array)Configure::read('Slugged');
  115. parent::__construct($table, $config);
  116. }
  117. /**
  118. * Constructor hook method.
  119. *
  120. * Implement this method to avoid having to overwrite
  121. * the constructor and call parent.
  122. *
  123. * @param array $config The configuration array this behavior is using.
  124. * @throws \RuntimeException
  125. * @return void
  126. */
  127. public function initialize(array $config): void {
  128. if ($this->_config['length'] === null) {
  129. $field = $this->_table->getSchema()->getColumn($this->_config['field']);
  130. $length = $field ? $field['length'] : 0;
  131. $this->_config['length'] = $length;
  132. }
  133. if (!$this->_config['label']) {
  134. $this->_config['label'] = $this->_table->getDisplayField();
  135. }
  136. $label = $this->_config['label'] = (array)$this->_config['label'];
  137. if ($this->_table->behaviors()->has('Translate')) {
  138. $this->_config['length'] = false;
  139. }
  140. if ($this->_config['length']) {
  141. foreach ($label as $field) {
  142. if (strpos($field, '.')) {
  143. [$alias, $field] = explode('.', $field);
  144. if (!$this->_table->$alias->hasField($field)) {
  145. throw new RuntimeException('(SluggedBehavior::setup) model `' . $this->_table->$alias->getAlias() . '` is missing the field `' . $field .
  146. '` (specified in the setup for table `' . $this->_table->getAlias() . '`) ');
  147. }
  148. } elseif (!$this->_table->hasField($field) && !method_exists($this->_table->getEntityClass(), '_get' . Inflector::classify($field))) {
  149. throw new RuntimeException('(SluggedBehavior::setup) model `' . $this->_table->getAlias() . '` is missing the field `' . $field .
  150. '` (specified in the setup for entity `' . $this->_table->getEntityClass() . '`.');
  151. }
  152. }
  153. }
  154. }
  155. /**
  156. * Customn finder exposed as
  157. *
  158. * ->find('slugged')
  159. *
  160. * @param \Cake\ORM\Query $query
  161. * @param array $options
  162. * @throws \InvalidArgumentException If the 'slug' key is missing in options
  163. * @return \Cake\ORM\Query
  164. */
  165. public function findSlugged(Query $query, array $options) {
  166. if (empty($options['slug'])) {
  167. throw new InvalidArgumentException("The 'slug' key is required for find('slugged')");
  168. }
  169. return $query->where([$this->_config['field'] => $options['slug']]);
  170. }
  171. /**
  172. * SluggedBehavior::beforeRules()
  173. *
  174. * @param \Cake\Event\EventInterface $event
  175. * @param \Cake\Datasource\EntityInterface $entity
  176. * @param \ArrayObject $options
  177. * @param string $operation
  178. *
  179. * @return void
  180. */
  181. public function beforeRules(EventInterface $event, EntityInterface $entity, ArrayObject $options, $operation) {
  182. if ($this->_config['on'] === 'beforeRules') {
  183. $this->slug($entity);
  184. }
  185. }
  186. /**
  187. * SluggedBehavior::beforeSave()
  188. *
  189. * @param \Cake\Event\EventInterface $event
  190. * @param \Cake\Datasource\EntityInterface $entity
  191. * @param \ArrayObject $options
  192. * @return void
  193. */
  194. public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options) {
  195. if ($this->_config['on'] === 'beforeSave') {
  196. $this->slug($entity);
  197. }
  198. }
  199. /**
  200. * SluggedBehavior::slug()
  201. *
  202. * @param \Cake\Datasource\EntityInterface $entity Entity
  203. * @param array $options Options
  204. * @return void
  205. */
  206. public function slug(EntityInterface $entity, array $options = []) {
  207. $overwrite = $options['overwrite'] ?? $this->_config['overwrite'];
  208. if (!$overwrite && $entity->get($this->_config['overwriteField'])) {
  209. $overwrite = true;
  210. }
  211. if ($overwrite || $entity->isNew() || !$entity->get($this->_config['field'])) {
  212. $pieces = [];
  213. foreach ((array)$this->_config['label'] as $v) {
  214. $v = $entity->get($v);
  215. if ($v !== null && $v !== '') {
  216. $pieces[] = $v;
  217. }
  218. }
  219. $slug = implode($this->_config['separator'], $pieces);
  220. $slug = $this->generateSlug($slug, $entity);
  221. $entity->set($this->_config['field'], $slug);
  222. }
  223. }
  224. /**
  225. * Method to find out if the current slug needs updating.
  226. *
  227. * The deep option is useful if you cannot rely on dirty() because
  228. * of maybe some not in sync slugs anymore (saving the same title again,
  229. * but the slug is completely different, for example).
  230. *
  231. * @param \Cake\Datasource\EntityInterface $entity
  232. * @param bool $deep If true it will generate a new slug and compare it to the currently stored one.
  233. * @return bool
  234. */
  235. public function needsSlugUpdate(EntityInterface $entity, $deep = false) {
  236. foreach ((array)$this->_config['label'] as $label) {
  237. if ($entity->isDirty($label)) {
  238. return true;
  239. }
  240. }
  241. if ($deep) {
  242. $copy = clone $entity;
  243. $this->slug($copy, ['overwrite' => true]);
  244. return $copy->get($this->_config['field']) !== $entity->get($this->_config['field']);
  245. }
  246. return false;
  247. }
  248. /**
  249. * Slug method
  250. *
  251. * For the given string, generate a slug. The replacements used are based on the mode setting, If tidy is false
  252. * (only possible if directly called - primarily for tracing and testing) separators will not be cleaned up
  253. * and so slugs like "-----as---df-----" are possible, which by default would otherwise be returned as "as-df".
  254. * If the mode is "id" and the first charcter of the regex-ed slug is numeric, it will be prefixed with an x.
  255. * If unique is set to true, check for a unique slug and if unavailable suffix the slug with -1, -2, -3 etc.
  256. * until a unique slug is found
  257. *
  258. * @param string $value
  259. * @param \Cake\Datasource\EntityInterface|null $entity
  260. * @throws \RuntimeException
  261. * @return string A slug
  262. */
  263. public function generateSlug($value, ?EntityInterface $entity = null) {
  264. $separator = $this->_config['separator'];
  265. $string = str_replace(["\r\n", "\r", "\n"], ' ', $value);
  266. $replace = $this->_config['replace'];
  267. if ($replace) {
  268. $string = str_replace(array_keys($replace), array_values($replace), $string);
  269. }
  270. if (!is_string($this->_config['mode'])) {
  271. $callable = $this->_config['mode'];
  272. if (!is_callable($callable)) {
  273. throw new RuntimeException('Invalid callable passed as mode.');
  274. }
  275. $slug = $callable($string);
  276. } elseif ($this->_config['mode'] === static::MODE_ASCII) {
  277. $slug = ShimInflector::slug($string, $separator);
  278. } elseif ($this->_config['mode'] === static::MODE_URL) {
  279. $regex = $this->_regex($this->_config['mode']);
  280. if ($regex) {
  281. $slug = $this->_pregReplace('@[' . $regex . ']@Su', $separator, $string);
  282. } else {
  283. $slug = $string;
  284. }
  285. } else {
  286. throw new RuntimeException('Invalid mode passed.');
  287. }
  288. if ($this->_config['tidy']) {
  289. $slug = $this->_pregReplace('/' . $separator . '+/', $separator, $slug);
  290. $slug = trim($slug, $separator);
  291. if ($slug && $this->_config['mode'] === 'id' && is_numeric($slug[0])) {
  292. $slug = 'x' . $slug;
  293. }
  294. }
  295. if ($this->_config['length'] && (mb_strlen($slug) > $this->_config['length'])) {
  296. $slug = mb_substr($slug, 0, $this->_config['length']);
  297. }
  298. if ($this->_config['case']) {
  299. $case = $this->_config['case'];
  300. if ($case === 'up') {
  301. $slug = mb_strtoupper($slug);
  302. } else {
  303. $slug = mb_strtolower($slug);
  304. }
  305. if (in_array($case, ['title', 'camel'])) {
  306. $words = explode($separator, $slug);
  307. foreach ($words as $i => &$word) {
  308. $firstChar = mb_substr($word, 0, 1);
  309. $rest = mb_substr($word, 1, mb_strlen($word) - 1);
  310. $firstCharUp = mb_strtoupper($firstChar);
  311. $word = $firstCharUp . $rest;
  312. }
  313. if ($case === 'title') {
  314. $slug = implode($separator, $words);
  315. } elseif ($case === 'camel') {
  316. $slug = implode('', $words);
  317. }
  318. }
  319. }
  320. if ($this->_config['unique']) {
  321. if (!$entity) {
  322. throw new RuntimeException('Needs an Entity to work on');
  323. }
  324. $field = $this->_table->getAlias() . '.' . $this->_config['field'];
  325. $conditions = [$field => $slug];
  326. $conditions = array_merge($conditions, $this->_config['scope']);
  327. $id = $entity->get($this->_table->getPrimaryKey());
  328. if ($id) {
  329. $conditions['NOT'][$this->_table->getAlias() . '.' . $this->_table->getPrimaryKey()] = $id;
  330. }
  331. $i = 0;
  332. $suffix = '';
  333. while ($this->_table->exists($conditions)) {
  334. $i++;
  335. $suffix = $separator . $i;
  336. if ($this->_config['length'] && (mb_strlen($slug . $suffix) > $this->_config['length'])) {
  337. $slug = mb_substr($slug, 0, $this->_config['length'] - mb_strlen($suffix));
  338. }
  339. $conditions[$field] = $slug . $suffix;
  340. }
  341. if ($suffix) {
  342. $slug .= $suffix;
  343. }
  344. }
  345. return $slug;
  346. }
  347. /**
  348. * ResetSlugs method.
  349. *
  350. * Regenerate all slugs. On large dbs this can take more than 30 seconds - a time
  351. * limit is set to allow a minimum 100 updates per second as a preventative measure.
  352. *
  353. * Note that you should use the Reset behavior if you need additional functionality such
  354. * as callbacks or timeouts.
  355. *
  356. * @param array $params
  357. * @throws \RuntimeException
  358. * @return bool Success
  359. */
  360. public function resetSlugs($params = []) {
  361. if (!$this->_table->hasField($this->_config['field'])) {
  362. throw new RuntimeException('Table does not have field ' . $this->_config['field']);
  363. }
  364. $defaults = [
  365. 'page' => 1,
  366. 'limit' => 100,
  367. 'fields' => array_merge([$this->_table->getPrimaryKey()], $this->_config['label']),
  368. 'order' => $this->_table->getDisplayField() . ' ASC',
  369. 'conditions' => $this->_config['scope'],
  370. 'overwrite' => true,
  371. ];
  372. $params = array_merge($defaults, $params);
  373. $conditions = $params['conditions'];
  374. $count = $this->_table->find('all', compact('conditions'))->count();
  375. $max = ini_get('max_execution_time');
  376. if ($max) {
  377. set_time_limit(max($max, $count / 100));
  378. }
  379. $this->setConfig($params, null, false);
  380. while (($records = $this->_table->find('all', $params)->toArray())) {
  381. /** @var \Cake\ORM\Entity $record */
  382. foreach ($records as $record) {
  383. $record->setNew(true);
  384. $fields = array_merge([$this->_table->getPrimaryKey(), $this->_config['field']], $this->_config['label']);
  385. $options = [
  386. 'validate' => true,
  387. 'fields' => $fields,
  388. ];
  389. if (!$this->_table->save($record, $options)) {
  390. throw new RuntimeException(print_r($record->getErrors(), true));
  391. }
  392. }
  393. $params['page']++;
  394. }
  395. return true;
  396. }
  397. /**
  398. * Multi slug method
  399. *
  400. * Handle both slug and label fields using the translate behavior, and being edited
  401. * in multiple locales at once
  402. *
  403. * //FIXME
  404. *
  405. * @param \Cake\Datasource\EntityInterface $entity
  406. * @return void
  407. */
  408. protected function _multiSlug(EntityInterface $entity) {
  409. $label = $this->getConfig('label');
  410. $field = current($label);
  411. $fields = (array)$entity->get($field);
  412. $locale = [];
  413. foreach ($fields as $locale => $_) {
  414. $res = null;
  415. foreach ($label as $field) {
  416. $res = $entity->get($field);
  417. if (is_array($entity->get($field))) {
  418. $res = $this->generateSlug($field[$locale], $entity);
  419. }
  420. }
  421. $locale[$locale] = $res;
  422. }
  423. $entity->set($this->getConfig('slugField'), $locale);
  424. }
  425. /**
  426. * Wrapper for preg replace taking care of encoding
  427. *
  428. * @param array|string $pattern
  429. * @param array|string $replace
  430. * @param string $string
  431. * @return string
  432. */
  433. protected function _pregReplace($pattern, $replace, $string) {
  434. return preg_replace($pattern, $replace, $string);
  435. }
  436. /**
  437. * Regex method
  438. *
  439. * Based upon the mode return a partial regex to generate a valid string for the intended use. Note that you
  440. * can use almost litterally anything in a url - the limitation is only in what your own application
  441. * understands. See the test case for info on how these regex patterns were generated.
  442. *
  443. * @param string $mode
  444. * @return string|null A partial regex or false on failure
  445. */
  446. protected function _regex($mode) {
  447. $return = '\x00-\x1f\x26\x3c\x7f-\x9f\x{fffe}-\x{ffff}';
  448. if ($mode === 'display') {
  449. return $return;
  450. }
  451. $return .= preg_quote(' \'"/?<>.$/:;?@=+&%\#,', '@');
  452. if ($mode === 'url') {
  453. return $return;
  454. }
  455. $return .= '';
  456. if ($mode === 'class') {
  457. return $return;
  458. }
  459. if ($mode === 'id') {
  460. return '\x{0000}-\x{002f}\x{003a}-\x{0040}\x{005b}-\x{005e}\x{0060}\x{007b}-\x{007e}\x{00a0}-\x{00b6}' .
  461. '\x{00b8}-\x{00bf}\x{00d7}\x{00f7}\x{0132}-\x{0133}\x{013f}-\x{0140}\x{0149}\x{017f}\x{01c4}-\x{01cc}' .
  462. '\x{01f1}-\x{01f3}\x{01f6}-\x{01f9}\x{0218}-\x{024f}\x{02a9}-\x{02ba}\x{02c2}-\x{02cf}\x{02d2}-\x{02ff}' .
  463. '\x{0346}-\x{035f}\x{0362}-\x{0385}\x{038b}\x{038d}\x{03a2}\x{03cf}\x{03d7}-\x{03d9}\x{03db}\x{03dd}\x{03df}' .
  464. '\x{03e1}\x{03f4}-\x{0400}\x{040d}\x{0450}\x{045d}\x{0482}\x{0487}-\x{048f}\x{04c5}-\x{04c6}\x{04c9}-\x{04ca}' .
  465. '\x{04cd}-\x{04cf}\x{04ec}-\x{04ed}\x{04f6}-\x{04f7}\x{04fa}-\x{0530}\x{0557}-\x{0558}\x{055a}-\x{0560}' .
  466. '\x{0587}-\x{0590}\x{05a2}\x{05ba}\x{05be}\x{05c0}\x{05c3}\x{05c5}-\x{05cf}\x{05eb}-\x{05ef}\x{05f3}-\x{0620}' .
  467. '\x{063b}-\x{063f}\x{0653}-\x{065f}\x{066a}-\x{066f}\x{06b8}-\x{06b9}\x{06bf}\x{06cf}\x{06d4}\x{06e9}' .
  468. '\x{06ee}-\x{06ef}\x{06fa}-\x{0900}\x{0904}\x{093a}-\x{093b}\x{094e}-\x{0950}\x{0955}-\x{0957}' .
  469. '\x{0964}-\x{0965}\x{0970}-\x{0980}\x{0984}\x{098d}-\x{098e}\x{0991}-\x{0992}\x{09a9}\x{09b1}\x{09b3}-\x{09b5}' .
  470. '\x{09ba}-\x{09bb}\x{09bd}\x{09c5}-\x{09c6}\x{09c9}-\x{09ca}\x{09ce}-\x{09d6}\x{09d8}-\x{09db}\x{09de}' .
  471. '\x{09e4}-\x{09e5}\x{09f2}-\x{0a01}\x{0a03}-\x{0a04}\x{0a0b}-\x{0a0e}\x{0a11}-\x{0a12}\x{0a29}\x{0a31}\x{0a34}' .
  472. '\x{0a37}\x{0a3a}-\x{0a3b}\x{0a3d}\x{0a43}-\x{0a46}\x{0a49}-\x{0a4a}\x{0a4e}-\x{0a58}\x{0a5d}\x{0a5f}-\x{0a65}' .
  473. '\x{0a75}-\x{0a80}\x{0a84}\x{0a8c}\x{0a8e}\x{0a92}\x{0aa9}\x{0ab1}\x{0ab4}\x{0aba}-\x{0abb}\x{0ac6}\x{0aca}' .
  474. '\x{0ace}-\x{0adf}\x{0ae1}-\x{0ae5}\x{0af0}-\x{0b00}\x{0b04}\x{0b0d}-\x{0b0e}\x{0b11}-\x{0b12}\x{0b29}\x{0b31}' .
  475. '\x{0b34}-\x{0b35}\x{0b3a}-\x{0b3b}\x{0b44}-\x{0b46}\x{0b49}-\x{0b4a}\x{0b4e}-\x{0b55}\x{0b58}-\x{0b5b}\x{0b5e}' .
  476. '\x{0b62}-\x{0b65}\x{0b70}-\x{0b81}\x{0b84}\x{0b8b}-\x{0b8d}\x{0b91}\x{0b96}-\x{0b98}\x{0b9b}\x{0b9d}' .
  477. '\x{0ba0}-\x{0ba2}\x{0ba5}-\x{0ba7}\x{0bab}-\x{0bad}\x{0bb6}\x{0bba}-\x{0bbd}\x{0bc3}-\x{0bc5}\x{0bc9}' .
  478. '\x{0bce}-\x{0bd6}\x{0bd8}-\x{0be6}\x{0bf0}-\x{0c00}\x{0c04}\x{0c0d}\x{0c11}\x{0c29}\x{0c34}\x{0c3a}-\x{0c3d}' .
  479. '\x{0c45}\x{0c49}\x{0c4e}-\x{0c54}\x{0c57}-\x{0c5f}\x{0c62}-\x{0c65}\x{0c70}-\x{0c81}\x{0c84}\x{0c8d}\x{0c91}' .
  480. '\x{0ca9}\x{0cb4}\x{0cba}-\x{0cbd}\x{0cc5}\x{0cc9}\x{0cce}-\x{0cd4}\x{0cd7}-\x{0cdd}\x{0cdf}\x{0ce2}-\x{0ce5}' .
  481. '\x{0cf0}-\x{0d01}\x{0d04}\x{0d0d}\x{0d11}\x{0d29}\x{0d3a}-\x{0d3d}\x{0d44}-\x{0d45}\x{0d49}\x{0d4e}-\x{0d56}' .
  482. '\x{0d58}-\x{0d5f}\x{0d62}-\x{0d65}\x{0d70}-\x{0e00}\x{0e2f}\x{0e3b}-\x{0e3f}\x{0e4f}\x{0e5a}-\x{0e80}\x{0e83}' .
  483. '\x{0e85}-\x{0e86}\x{0e89}\x{0e8b}-\x{0e8c}\x{0e8e}-\x{0e93}\x{0e98}\x{0ea0}\x{0ea4}\x{0ea6}\x{0ea8}-\x{0ea9}' .
  484. '\x{0eac}\x{0eaf}\x{0eba}\x{0ebe}-\x{0ebf}\x{0ec5}\x{0ec7}\x{0ece}-\x{0ecf}\x{0eda}-\x{0f17}\x{0f1a}-\x{0f1f}' .
  485. '\x{0f2a}-\x{0f34}\x{0f36}\x{0f38}\x{0f3a}-\x{0f3d}\x{0f48}\x{0f6a}-\x{0f70}\x{0f85}\x{0f8c}-\x{0f8f}\x{0f96}' .
  486. '\x{0f98}\x{0fae}-\x{0fb0}\x{0fb8}\x{0fba}-\x{109f}\x{10c6}-\x{10cf}\x{10f7}-\x{10ff}\x{1101}\x{1104}\x{1108}' .
  487. '\x{110a}\x{110d}\x{1113}-\x{113b}\x{113d}\x{113f}\x{1141}-\x{114b}\x{114d}\x{114f}\x{1151}-\x{1153}' .
  488. '\x{1156}-\x{1158}\x{115a}-\x{115e}\x{1162}\x{1164}\x{1166}\x{1168}\x{116a}-\x{116c}\x{116f}-\x{1171}\x{1174}' .
  489. '\x{1176}-\x{119d}\x{119f}-\x{11a7}\x{11a9}-\x{11aa}\x{11ac}-\x{11ad}\x{11b0}-\x{11b6}\x{11b9}\x{11bb}' .
  490. '\x{11c3}-\x{11ea}\x{11ec}-\x{11ef}\x{11f1}-\x{11f8}\x{11fa}-\x{1dff}\x{1e9c}-\x{1e9f}\x{1efa}-\x{1eff}' .
  491. '\x{1f16}-\x{1f17}\x{1f1e}-\x{1f1f}\x{1f46}-\x{1f47}\x{1f4e}-\x{1f4f}\x{1f58}\x{1f5a}\x{1f5c}\x{1f5e}' .
  492. '\x{1f7e}-\x{1f7f}\x{1fb5}\x{1fbd}\x{1fbf}-\x{1fc1}\x{1fc5}\x{1fcd}-\x{1fcf}\x{1fd4}-\x{1fd5}\x{1fdc}-\x{1fdf}' .
  493. '\x{1fed}-\x{1ff1}\x{1ff5}\x{1ffd}-\x{20cf}\x{20dd}-\x{20e0}\x{20e2}-\x{2125}\x{2127}-\x{2129}' .
  494. '\x{212c}-\x{212d}\x{212f}-\x{217f}\x{2183}-\x{3004}\x{3006}\x{3008}-\x{3020}\x{3030}\x{3036}-\x{3040}' .
  495. '\x{3095}-\x{3098}\x{309b}-\x{309c}\x{309f}-\x{30a0}\x{30fb}\x{30ff}-\x{3104}\x{312d}-\x{4dff}' .
  496. '\x{9fa6}-\x{abff}\x{d7a4}-\x{d7ff}\x{e000}-\x{ffff}';
  497. }
  498. return null;
  499. }
  500. }