SluggedBehavior.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657
  1. <?php
  2. /**
  3. * Short description for slugged.php
  4. *
  5. * Part based/inspired by the sluggable behavior of Mariano Iglesias
  6. *
  7. * PHP version 5
  8. *
  9. * Copyright (c) 2008, Andy Dawson
  10. *
  11. * Licensed under The MIT License
  12. * Redistributions of files must retain the above copyright notice.
  13. *
  14. * @copyright Copyright (c) 2008, Andy Dawson
  15. * @link www.ad7six.com
  16. * @package mi
  17. * @subpackage mi.models.behaviors
  18. * @since v 1.0
  19. * @license http://www.opensource.org/licenses/mit-license.php The MIT License
  20. */
  21. /**
  22. * Ensure that mb_ functions exist
  23. */
  24. App::import('I18n', 'Multibyte');
  25. App::uses('ModelBehavior', 'Model');
  26. /**
  27. * SluggedBehavior class
  28. *
  29. * @uses ModelBehavior
  30. * @version 2.x
  31. * @modified Mark Scherer
  32. */
  33. class SluggedBehavior extends ModelBehavior {
  34. /**
  35. * defaultSettings property
  36. *
  37. * label
  38. * 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
  39. * on the format returned by find('list') to determine the string to use for slugs
  40. * overwrite has 2 values
  41. * false - once the slug has been saved, do not change it (use if you are doing lookups based on slugs)
  42. * true - if the label field values change, regenerate the slug (use if you are the slug is just window-dressing)
  43. * unique has 2 values
  44. * false - will not enforce a unique slug, whatever the label is is direclty slugged without checking for duplicates
  45. * true - use if you are doing lookups based on slugs (see overwrite)
  46. * mode has the following values
  47. * ascii - retuns an ascii slug generated using the core Inflector::slug() function
  48. * display - a dummy mode which returns a slug legal for display - removes illegal (not unprintable) characters
  49. * url - returns a slug appropriate to put in a url
  50. * class - a dummy mode which returns a slug appropriate to put in a html class (there are no restrictions)
  51. * id - retuns a slug appropriate to use in a html id
  52. * case has the following values
  53. * null - don't change the case of the slug
  54. * low - force lower case. E.g. "this-is-the-slug"
  55. * up - force upper case E.g. "THIS-IS-THE-SLUG"
  56. * title - force title case. E.g. "This-Is-The-Slug"
  57. * camel - force CamelCase. E.g. "ThisIsTheSlug"
  58. *
  59. * @var array
  60. * @access protected
  61. */
  62. protected $_defaultSettings = array(
  63. 'label' => null,
  64. 'slugField' => 'slug',
  65. 'overwriteField' => 'overwrite_slug',
  66. 'mode' => 'url',
  67. 'separator' => '-',
  68. 'defaultSuffix' => null,
  69. 'length' => 100,
  70. 'overwrite' => false,
  71. 'unique' => false,
  72. 'notices' => true,
  73. 'case' => null,
  74. 'replace' => array(
  75. '&' => 'and',
  76. '+' => 'and',
  77. '#' => 'hash',
  78. ),
  79. 'run' => 'beforeValidate',
  80. 'language' => null,
  81. 'encoding' => null,
  82. 'trigger' => false,
  83. 'scope' => array()
  84. );
  85. /**
  86. * stopWords property
  87. *
  88. * A (3 letter) language code indexed array of stop worlds
  89. *
  90. * @var array
  91. * @access public
  92. */
  93. public $stopWords = array();
  94. /**
  95. * setup method
  96. *
  97. * Use the model's label field as the default field on which to base the slug, the label can be made up of multiple
  98. * fields by specifying an array of fields
  99. *
  100. * @param mixed $Model
  101. * @param array $config
  102. * @access public
  103. * @return void
  104. */
  105. public function setup(Model $Model, $config = array()) {
  106. $this->_defaultSettings['notices'] = Configure::read('debug');
  107. $this->_defaultSettings['label'] = array($Model->displayField);
  108. foreach ($this->_defaultSettings['replace'] as $key => $value) {
  109. $this->_defaultSettings['replace'][$key] = __($value);
  110. }
  111. $this->_defaultSettings = Set::merge($this->_defaultSettings, (array)Configure::read('Slugged'));
  112. $this->settings[$Model->alias] = Set::merge($this->_defaultSettings, $config);
  113. extract($this->settings[$Model->alias]);
  114. $label = $this->settings[$Model->alias]['label'] = (array)$label;
  115. if ($Model->Behaviors->attached('Translate')) {
  116. $notices = false;
  117. }
  118. if ($notices) {
  119. foreach ($label as $field) {
  120. $alias = $Model->alias;
  121. if (strpos($field, '.')) {
  122. list($alias, $field) = explode('.', $field);
  123. if (!$Model->$alias->hasField($field)) {
  124. trigger_error('(SluggedBehavior::setup) model ' . $Model->$alias->name . ' is missing the field ' . $field .
  125. ' (specified in the setup for model ' . $Model->name . ') ', E_USER_WARNING);
  126. $Model->Behaviors->disable($this->name);
  127. }
  128. } elseif (!$Model->hasField($field)) {
  129. trigger_error('(SluggedBehavior::setup) model ' . $Model->name . ' is missing the field ' . $field . ' specified in the setup.', E_USER_WARNING);
  130. $Model->Behaviors->disable($this->name);
  131. }
  132. }
  133. }
  134. }
  135. /**
  136. * beforeValidate method
  137. *
  138. * @param mixed $Model
  139. * @return void
  140. * @access public
  141. */
  142. public function beforeValidate(Model $Model) {
  143. extract($this->settings[$Model->alias]);
  144. if ($run !== 'beforeValidate') {
  145. return true;
  146. }
  147. if (is_string($this->settings[$Model->alias]['trigger'])) {
  148. if (!$Model->{$this->settings[$Model->alias]['trigger']}) {
  149. return true;
  150. }
  151. }
  152. return $this->generateSlug($Model);
  153. }
  154. /**
  155. * beforeSave method
  156. *
  157. * @param mixed $Model
  158. * @return void
  159. * @access public
  160. */
  161. public function beforeSave(Model $Model) {
  162. extract($this->settings[$Model->alias]);
  163. if ($run !== 'beforeSave') {
  164. return true;
  165. }
  166. if (is_string($this->settings[$Model->alias]['trigger'])) {
  167. if (!$Model->{$this->settings[$Model->alias]['trigger']}) {
  168. return true;
  169. }
  170. }
  171. return $this->generateSlug($Model);
  172. }
  173. /**
  174. * generate slug method
  175. *
  176. * if a new row, or overwrite is set to true, check for a change to a label field and add the slug to the data
  177. * to be saved
  178. * If no slug at all is returned (should not be permitted and prevented by validating the label fields) the model
  179. * alias will be used as a slug.
  180. * If unique is set to true, check for a unique slug and if unavailable suffix the slug with -1, -2, -3 etc.
  181. * until a unique slug is found
  182. *
  183. * @param mixed $Model
  184. * @access public
  185. * @return void
  186. */
  187. public function generateSlug(Model $Model) {
  188. extract($this->settings[$Model->alias]);
  189. if ($notices && !$Model->hasField($slugField)) {
  190. return true;
  191. }
  192. if (!$overwrite && !empty($Model->data[$Model->alias][$overwriteField])) {
  193. $overwrite = true;
  194. }
  195. if ($overwrite || !$Model->id) {
  196. if ($label) {
  197. $somethingToDo = false;
  198. foreach ($label as $field) {
  199. $alias = $Model->alias;
  200. if (strpos($field, '.') !== false) {
  201. list($alias, $field) = explode('.', $field, 2);
  202. }
  203. if (isset($Model->data[$alias][$field])) {
  204. $somethingToDo = true;
  205. }
  206. }
  207. if (!$somethingToDo) {
  208. return true;
  209. }
  210. $slug = array();
  211. foreach ($label as $field) {
  212. $alias = $Model->alias;
  213. if (strpos($field, '.')) {
  214. list($alias, $field) = explode('.', $field);
  215. }
  216. if (isset($Model->data[$alias][$field])) {
  217. if (is_array($Model->data[$alias][$field])) {
  218. return $this->_multiSlug($Model);
  219. }
  220. $slug[] = $Model->data[$alias][$field];
  221. } elseif ($Model->id) {
  222. $slug[] = $Model->field($field);
  223. }
  224. }
  225. $slug = implode($slug, $separator);
  226. } else {
  227. $slug = $this->display($Model);
  228. }
  229. $slug = $Model->slug($slug);
  230. if (!$slug) {
  231. $slug = $Model->alias;
  232. }
  233. if ($unique) {
  234. $conditions = array($Model->alias . '.' . $slugField => $slug);
  235. $conditions = array_merge($conditions, $this->settings[$Model->alias]['scope']);
  236. if ($Model->id) {
  237. $conditions['NOT'][$Model->alias . '.' . $Model->primaryKey] = $Model->id;
  238. }
  239. $i = 0;
  240. $suffix = '';
  241. while ($Model->hasAny($conditions)) {
  242. $i++;
  243. $suffix = $separator . $i;
  244. if (strlen($slug . $suffix) > $length) {
  245. $slug = substr($slug, 0, $length - strlen($suffix));
  246. }
  247. $conditions[$Model->alias . '.' . $slugField] = $slug . $suffix;
  248. }
  249. if ($suffix) {
  250. $slug .= $suffix;
  251. }
  252. }
  253. $this->_addToWhitelist($Model, array($slugField));
  254. $Model->data[$Model->alias][$slugField] = $slug;
  255. }
  256. return true;
  257. }
  258. /**
  259. * removeStopWords from a string. if $splitOnStopWord is true, the following occurs:
  260. * input "apples bananas pears and red cars"
  261. * output array('apples bananas pears', 'red cars')
  262. *
  263. * If the passed string doesn't contain the separator, or after stripping out stop words there's
  264. * nothing left - the original input is returned (in the desired format)
  265. *
  266. * Therefore passing "contain" will return immediately array('contain')
  267. * Passing "contain this text" will return array('text')
  268. * both contain and this are stop words
  269. * Passing "contain this" will return array('contain this')
  270. *
  271. * @param mixed $Model
  272. * @param mixed $string string or array of words
  273. * @param array $params
  274. * @return mixed
  275. * @access public
  276. */
  277. public function removeStopWords(Model $Model, $string = '', $params = array()) {
  278. if (!$string) {
  279. return $string;
  280. }
  281. $separator = ' ';
  282. $splitOnStopWord = true;
  283. $return = 'array';
  284. $originalIfEmpty = true;
  285. extract($params);
  286. /*
  287. if (!class_exists('MiCache')) {
  288. App::import('Vendor', 'Mi.MiCache');
  289. }
  290. */
  291. if (!empty($this->settings[$Model->alias]['language'])) {
  292. $lang = $this->settings[$Model->alias]['language'];
  293. } else {
  294. $lang = Configure::read('Site.lang');
  295. if (!$lang) {
  296. $lang = 'eng';
  297. }
  298. $this->settings[$Model->alias]['language'] = $lang;
  299. }
  300. if (!array_key_exists($lang, $this->stopWords)) {
  301. ob_start();
  302. if (!App::import('Vendor', 'stop_words_' . $lang, array('file' => "stop_words".DS."$lang.txt"))) {
  303. $res = App::import('Vendor', 'Tools.stop_words_' . $lang, array('file' => "stop_words".DS."$lang.txt"));
  304. }
  305. $stopWords = preg_replace('@/\*.*\*/@', '', ob_get_clean());
  306. $this->stopWords[$lang] = array_filter(array_map('trim', explode("\n", $stopWords)));
  307. }
  308. if (is_array($string)) {
  309. $originalTerms = $terms = $string;
  310. foreach ($terms as $i => &$term) {
  311. $term = trim(preg_replace('@[^\p{Ll}\p{Lm}\p{Lo}\p{Lt}\p{Lu}]@u', $separator, $term), $separator);
  312. }
  313. $lTerms = array_map('mb_strtolower', $terms);
  314. $lTerms = array_diff($lTerms, $this->stopWords[$lang]);
  315. $terms = array_intersect_key($terms, $lTerms);
  316. } else {
  317. if (!strpos($string, $separator)) {
  318. if ($return === 'array') {
  319. return array($string);
  320. }
  321. return $string;
  322. }
  323. $string = preg_replace('@[^\p{Ll}\p{Lm}\p{Lo}\p{Lt}\p{Lu}]@u', $separator, $string);
  324. $originalTerms = $terms = array_filter(array_map('trim', explode($separator, $string)));
  325. if ($splitOnStopWord) {
  326. $terms = $chunk = array();
  327. $snippet = '';
  328. foreach ($originalTerms as $term) {
  329. $lterm = strtolower($term);
  330. if (in_array($lterm, $this->stopWords[$lang])) {
  331. if ($chunk) {
  332. $terms[] = $chunk;
  333. $chunk = array();
  334. }
  335. continue;
  336. }
  337. $chunk[] = $term;
  338. }
  339. if ($chunk) {
  340. $terms[] = $chunk;
  341. }
  342. foreach ($terms as &$phrase) {
  343. $phrase = implode(' ', $phrase);
  344. }
  345. } else {
  346. $lTerms = array_map('mb_strtolower', $terms);
  347. $lTerms = array_diff($lTerms, $this->stopWords[$lang]);
  348. $terms = array_intersect_key($terms, $lTerms);
  349. }
  350. }
  351. if (!$terms && $originalIfEmpty) {
  352. $terms = array(implode(' ', $originalTerms));
  353. }
  354. if ($return === 'array') {
  355. return array_values(array_unique($terms));
  356. }
  357. return implode($separator, $terms);
  358. }
  359. /**
  360. * slug method
  361. *
  362. * For the given string, generate a slug. The replacements used are based on the mode setting, If tidy is false
  363. * (only possible if directly called - primarily for tracing and testing) separators will not be cleaned up
  364. * and so slugs like "-----as---df-----" are possible, which by default would otherwise be returned as "as-df".
  365. * If the mode is "id" and the first charcter of the regex-ed slug is numeric, it will be prefixed with an x.
  366. *
  367. * @param mixed $Model
  368. * @param mixed $string
  369. * @param bool $tidy
  370. * @return string a slug
  371. * @access public
  372. */
  373. public function slug(Model $Model, $string, $tidy = true) {
  374. extract($this->settings[$Model->alias]);
  375. $this->_setEncoding($Model, $encoding, $string, !Configure::read('debug'));
  376. if ($replace) {
  377. $string = str_replace(array_keys($replace), array_values($replace), $string);
  378. }
  379. if ($mode === 'ascii') {
  380. $slug = Inflector::slug($string, $separator);
  381. } else {
  382. $regex = $this->_regex($mode);
  383. if ($regex) {
  384. $slug = $this->_pregReplace('@[' . $regex . ']@Su', $separator, $string, $encoding);
  385. } else {
  386. $slug = $string;
  387. }
  388. }
  389. if ($tidy) {
  390. $slug = $this->_pregReplace('/' . $separator . '+/', $separator, $slug, $encoding);
  391. $slug = trim($slug, $separator);
  392. if ($slug && $mode === 'id' && is_numeric($slug[0])) {
  393. $slug = 'x' . $slug;
  394. }
  395. }
  396. if (strlen($slug) > $length) {
  397. $slug = mb_substr ($slug, 0, $length);
  398. while ($slug && strlen($slug) > $length) {
  399. $slug = mb_substr ($slug, 0, mb_strlen($slug) - 1);
  400. }
  401. }
  402. if ($case) {
  403. if ($case === 'up') {
  404. $slug = mb_strtoupper($slug);
  405. } else {
  406. $slug = mb_strtolower($slug);
  407. }
  408. if (in_array($case, array('title', 'camel'))) {
  409. $words = explode($separator, $slug);
  410. foreach ($words as $i => &$word) {
  411. $firstChar = mb_substr($word, 0, 1);
  412. $rest = mb_substr($word, 1, mb_strlen($word) - 1);
  413. $firstCharUp = mb_strtoupper($firstChar);
  414. $word = $firstCharUp . $rest;
  415. }
  416. if ($case === 'title') {
  417. $slug = implode($words, $separator);
  418. } elseif ($case === 'camel') {
  419. $slug = implode($words);
  420. }
  421. }
  422. }
  423. return $slug;
  424. }
  425. /**
  426. * display method
  427. *
  428. * Cheat - use find('list') and assume it has been modified such that lists show in the desired format.
  429. * First check (since this method is called in beforeSave) if there is data to be saved, and use that
  430. * to get the display name
  431. * Otherwise, read from the database
  432. *
  433. * @param mixed $id
  434. * @return mixed string (the display name) or false
  435. * @access public
  436. */
  437. public function display(Model $Model, $id = null) {
  438. if (!$id) {
  439. if (!$Model->id) {
  440. return false;
  441. }
  442. $id = $Model->id;
  443. }
  444. $conditions = array_merge(array(
  445. $Model->alias . '.' . $Model->primaryKey => $id),
  446. $this->settings[$Model->alias]['scope']);
  447. return current($Model->find('list', array('conditions' => $conditions)));
  448. }
  449. /**
  450. * resetSlugs method
  451. *
  452. * Regenerate all slugs. On large dbs this can take more than 30 seconds - a time limit is set to allow a minimum
  453. * 100 updates per second as a preventative measure.
  454. *
  455. * @param AppModel $Model
  456. * @param array $conditions
  457. * @param int $recursive
  458. * @return bool true on success false otherwise
  459. * @access public
  460. */
  461. public function resetSlugs(Model $Model, $params = array()) {
  462. $recursive = -1;
  463. extract($this->settings[$Model->alias]);
  464. if ($notices && !$Model->hasField($slugField)) {
  465. return false;
  466. }
  467. $defaults = array(
  468. 'page' => 1,
  469. 'limit' => 100,
  470. 'fields' => array_merge(array($Model->primaryKey, $slugField), $label),
  471. 'order' => $Model->displayField . ' ASC',
  472. 'conditions' => $scope,
  473. 'recursive' => $recursive,
  474. 'overwrite' => true,
  475. );
  476. $params = array_merge($defaults, $params);
  477. $count = $Model->find('count', compact('conditions'));
  478. $max = ini_get('max_execution_time');
  479. if ($max) {
  480. set_time_limit(max($max, $count / 100));
  481. }
  482. while ($rows = $Model->find('all', $params)) {
  483. foreach ($rows as $row) {
  484. $Model->create();
  485. if (!$Model->save($row, true, array($slugField))) {
  486. throw new RuntimeException(print_r($Model->validationErrors, true));
  487. }
  488. }
  489. $params['page']++;
  490. }
  491. return true;
  492. }
  493. /**
  494. * Multi slug method
  495. *
  496. * Handle both slug and lable fields using the translate behavior, and being edited
  497. * in multiple locales at once
  498. *
  499. * @param mixed $Model
  500. * @return void
  501. * @access protected
  502. */
  503. protected function _multiSlug(Model $Model) {
  504. extract($this->settings[$Model->alias]);
  505. $data = $Model->data;
  506. $field = current($label);
  507. foreach ($Model->data[$Model->alias][$field] as $locale => $_) {
  508. foreach ($label as $field) {
  509. if (is_array($data[$Model->alias][$field])) {
  510. $Model->data[$Model->alias][$field] = $data[$Model->alias][$field][$locale];
  511. }
  512. }
  513. $this->beforeValidate($Model);
  514. $data[$Model->alias][$slugField][$locale] = $Model->data[$Model->alias][$field];
  515. }
  516. $Model->data = $data;
  517. return true;
  518. }
  519. /**
  520. * Wrapper for preg replace taking care of encoding
  521. *
  522. * @param mixed $pattern
  523. * @param mixed $replace
  524. * @param mixed $string
  525. * @param string $encoding 'UTF-8'
  526. * @return void
  527. * @access protected
  528. */
  529. protected function _pregReplace($pattern, $replace, $string, $encoding = 'UTF-8') {
  530. if ($encoding && $encoding !== 'UTF-8') {
  531. $string = mb_convert_encoding($string, 'UTF-8', $encoding);
  532. }
  533. $return = preg_replace($pattern, $replace, $string);
  534. if ($encoding && $encoding !== 'UTF-8') {
  535. $return = mb_convert_encoding($return, $encoding, 'UTF-8');
  536. }
  537. return $return;
  538. }
  539. /**
  540. * setEncoding method
  541. *
  542. * @param mixed $Model
  543. * @param mixed $encoding null
  544. * @param mixed $string
  545. * @param mixed $reset null
  546. * @return void
  547. * @access protected
  548. */
  549. protected function _setEncoding(Model $Model, &$encoding = null, &$string, $reset = null) {
  550. if (function_exists('mb_internal_encoding')) {
  551. $aEncoding = Configure::read('App.encoding');
  552. if ($aEncoding) {
  553. if (!$encoding) {
  554. $encoding = $aEncoding;
  555. } elseif ($encoding !== $aEncoding) {
  556. $string = mb_convert_encoding($string, $encoding, $aEncoding);
  557. }
  558. } else {
  559. $encoding = $aEncoding;
  560. }
  561. if ($encoding) {
  562. mb_internal_encoding($encoding);
  563. }
  564. }
  565. }
  566. /**
  567. * regex method
  568. *
  569. * Based upon the mode return a partial regex to generate a valid string for the intended use. Note that you
  570. * can use almost litterally anything in a url - the limitation is only in what your own application
  571. * understands. See the test case for info on how these regex patterns were generated.
  572. *
  573. * @param string $mode
  574. * @return string a partial regex
  575. * @access private
  576. */
  577. protected function _regex($mode) {
  578. $return = '\x00-\x1f\x26\x3c\x7f-\x9f\x{d800}-\x{dfff}\x{fffe}-\x{ffff}';
  579. if ($mode === 'display') {
  580. return $return;
  581. }
  582. $return .= preg_quote(' \'"/?<>.$/:;?@=+&%\#', '@');
  583. if ($mode === 'url') {
  584. return $return;
  585. }
  586. $return .= '';
  587. if ($mode === 'class') {
  588. return $return;
  589. }
  590. if ($mode === 'id') {
  591. return '\x{0000}-\x{002f}\x{003a}-\x{0040}\x{005b}-\x{005e}\x{0060}\x{007b}-\x{007e}\x{00a0}-\x{00b6}' .
  592. '\x{00b8}-\x{00bf}\x{00d7}\x{00f7}\x{0132}-\x{0133}\x{013f}-\x{0140}\x{0149}\x{017f}\x{01c4}-\x{01cc}' .
  593. '\x{01f1}-\x{01f3}\x{01f6}-\x{01f9}\x{0218}-\x{024f}\x{02a9}-\x{02ba}\x{02c2}-\x{02cf}\x{02d2}-\x{02ff}' .
  594. '\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}' .
  595. '\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}' .
  596. '\x{04cd}-\x{04cf}\x{04ec}-\x{04ed}\x{04f6}-\x{04f7}\x{04fa}-\x{0530}\x{0557}-\x{0558}\x{055a}-\x{0560}' .
  597. '\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}' .
  598. '\x{063b}-\x{063f}\x{0653}-\x{065f}\x{066a}-\x{066f}\x{06b8}-\x{06b9}\x{06bf}\x{06cf}\x{06d4}\x{06e9}' .
  599. '\x{06ee}-\x{06ef}\x{06fa}-\x{0900}\x{0904}\x{093a}-\x{093b}\x{094e}-\x{0950}\x{0955}-\x{0957}' .
  600. '\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}' .
  601. '\x{09ba}-\x{09bb}\x{09bd}\x{09c5}-\x{09c6}\x{09c9}-\x{09ca}\x{09ce}-\x{09d6}\x{09d8}-\x{09db}\x{09de}' .
  602. '\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}' .
  603. '\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}' .
  604. '\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}' .
  605. '\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}' .
  606. '\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}' .
  607. '\x{0b62}-\x{0b65}\x{0b70}-\x{0b81}\x{0b84}\x{0b8b}-\x{0b8d}\x{0b91}\x{0b96}-\x{0b98}\x{0b9b}\x{0b9d}' .
  608. '\x{0ba0}-\x{0ba2}\x{0ba5}-\x{0ba7}\x{0bab}-\x{0bad}\x{0bb6}\x{0bba}-\x{0bbd}\x{0bc3}-\x{0bc5}\x{0bc9}' .
  609. '\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}' .
  610. '\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}' .
  611. '\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}' .
  612. '\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}' .
  613. '\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}' .
  614. '\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}' .
  615. '\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}' .
  616. '\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}' .
  617. '\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}' .
  618. '\x{110a}\x{110d}\x{1113}-\x{113b}\x{113d}\x{113f}\x{1141}-\x{114b}\x{114d}\x{114f}\x{1151}-\x{1153}' .
  619. '\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}' .
  620. '\x{1176}-\x{119d}\x{119f}-\x{11a7}\x{11a9}-\x{11aa}\x{11ac}-\x{11ad}\x{11b0}-\x{11b6}\x{11b9}\x{11bb}' .
  621. '\x{11c3}-\x{11ea}\x{11ec}-\x{11ef}\x{11f1}-\x{11f8}\x{11fa}-\x{1dff}\x{1e9c}-\x{1e9f}\x{1efa}-\x{1eff}' .
  622. '\x{1f16}-\x{1f17}\x{1f1e}-\x{1f1f}\x{1f46}-\x{1f47}\x{1f4e}-\x{1f4f}\x{1f58}\x{1f5a}\x{1f5c}\x{1f5e}' .
  623. '\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}' .
  624. '\x{1fed}-\x{1ff1}\x{1ff5}\x{1ffd}-\x{20cf}\x{20dd}-\x{20e0}\x{20e2}-\x{2125}\x{2127}-\x{2129}' .
  625. '\x{212c}-\x{212d}\x{212f}-\x{217f}\x{2183}-\x{3004}\x{3006}\x{3008}-\x{3020}\x{3030}\x{3036}-\x{3040}' .
  626. '\x{3095}-\x{3098}\x{309b}-\x{309c}\x{309f}-\x{30a0}\x{30fb}\x{30ff}-\x{3104}\x{312d}-\x{4dff}' .
  627. '\x{9fa6}-\x{abff}\x{d7a4}-\x{d7ff}\x{e000}-\x{ffff}';
  628. }
  629. return false;
  630. }
  631. }