TypographicBehavior.php 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. <?php
  2. /**
  3. * PHP 5
  4. *
  5. * @author Mark Scherer
  6. * @license http://opensource.org/licenses/mit-license.php MIT
  7. */
  8. App::uses('ModelBehavior', 'Model');
  9. /**
  10. * Replace regionalized chars with standard ones on input.
  11. *
  12. * “smart quotes” become "dumb quotes" on save
  13. * „low-high“ become "high-high"
  14. * same for single quotes (apostrophes)
  15. * in order to unify them. Basic idea is a unified non-regional version in the database.
  16. *
  17. * Using the TypographyHelper we can then format the output
  18. * according to the language/regional setting (in some languages
  19. * the high-high smart quotes, in others the low-high ones are preferred)
  20. *
  21. * Settings are:
  22. * - string $before (validate/save)
  23. * - array $fields (leave empty for auto detection)
  24. * - bool $mergeQuotes (merge single and double into " or any custom char)
  25. *
  26. * TODOS:
  27. * - respect primary and secondary quotations marks as well as alternatives
  28. *
  29. * @link http://www.dereuromark.de/2012/08/12/typographic-behavior-and-typography-helper/
  30. * @link http://en.wikipedia.org/wiki/Non-English_usage_of_quotation_marks
  31. */
  32. class TypographicBehavior extends ModelBehavior {
  33. protected $_map = [
  34. 'in' => [
  35. '‘' => '\'',
  36. // Translates to '&lsquo;'.
  37. '’' => '\'',
  38. // Translates to '&rsquo;'.
  39. '‚' => '\'',
  40. // Translates to '&sbquo;'.
  41. '‛' => '\'',
  42. // Translates to '&#8219;'.
  43. '“' => '"',
  44. // Translates to '&ldquo;'.
  45. '”' => '"',
  46. // Translates to '&rdquo;'.
  47. '„' => '"',
  48. // Translates to '&bdquo;'.
  49. '‟' => '"',
  50. // Translates to '&#8223;'.
  51. '«' => '"',
  52. // Translates to '&laquo;'.
  53. '»' => '"',
  54. // Translates to '&raquo;'.
  55. '‹' => '\'',
  56. // Translates to '&laquo;'.
  57. '›' => '\'',
  58. // Translates to '&raquo;'.
  59. ],
  60. 'out' => [
  61. // Use the TypographyHelper for this at runtime.
  62. ],
  63. ];
  64. protected $_defaultConfig = [
  65. 'before' => 'save',
  66. 'fields' => [],
  67. 'mergeQuotes' => false, // Set to true for " or explicitly set a char (" or ').
  68. ];
  69. /**
  70. * Initiate behavior for the model using specified settings.
  71. * Available settings:
  72. *
  73. * @param Model $Model Model using the behaviour
  74. * @param array $config Settings to override for model.
  75. * @return void
  76. */
  77. public function setup(Model $Model, $config = []) {
  78. if (!isset($this->settings[$Model->alias])) {
  79. $this->settings[$Model->alias] = $this->_defaultConfig;
  80. }
  81. $this->settings[$Model->alias] = $config + $this->settings[$Model->alias];
  82. if (empty($this->settings[$Model->alias]['fields'])) {
  83. $schema = $Model->schema();
  84. $fields = [];
  85. foreach ($schema as $field => $v) {
  86. if (!in_array($v['type'], ['string', 'text'])) {
  87. continue;
  88. }
  89. if (!empty($v['key'])) {
  90. continue;
  91. }
  92. if (isset($v['length']) && $v['length'] === 1) { // TODO: also skip UUID (lenght 36)?
  93. continue;
  94. }
  95. $fields[] = $field;
  96. }
  97. $this->settings[$Model->alias]['fields'] = $fields;
  98. }
  99. if ($this->settings[$Model->alias]['mergeQuotes'] === true) {
  100. $this->settings[$Model->alias]['mergeQuotes'] = '"';
  101. }
  102. }
  103. /**
  104. * TypographicBehavior::beforeValidate()
  105. *
  106. * @param Model $Model
  107. * @return bool Success
  108. */
  109. public function beforeValidate(Model $Model, $options = []) {
  110. parent::beforeValidate($Model, $options);
  111. if ($this->settings[$Model->alias]['before'] === 'validate') {
  112. $this->process($Model);
  113. }
  114. return true;
  115. }
  116. /**
  117. * TypographicBehavior::beforeSave()
  118. *
  119. * @param Model $Model
  120. * @return bool Success
  121. */
  122. public function beforeSave(Model $Model, $options = []) {
  123. parent::beforeSave($Model, $options);
  124. if ($this->settings[$Model->alias]['before'] === 'save') {
  125. $this->process($Model);
  126. }
  127. return true;
  128. }
  129. /**
  130. * Run the behavior over all records of this model
  131. * This is useful if you attach it after some records have already been saved without it.
  132. *
  133. * @param Model $Model The model about to be saved.
  134. * @return int count Number of affected/changed records
  135. */
  136. public function updateTypography(Model $Model, $dryRun = false) {
  137. $options = ['recursive' => -1, 'limit' => 100, 'offset' => 0];
  138. $count = 0;
  139. while ($records = $Model->find('all', $options)) {
  140. foreach ($records as $record) {
  141. $changed = false;
  142. foreach ($this->settings[$Model->alias]['fields'] as $field) {
  143. if (empty($record[$Model->alias][$field])) {
  144. continue;
  145. }
  146. $tmp = $this->_prepareInput($Model, $record[$Model->alias][$field]);
  147. if ($tmp == $record[$Model->alias][$field]) {
  148. continue;
  149. }
  150. $record[$Model->alias][$field] = $tmp;
  151. $changed = true;
  152. }
  153. if ($changed) {
  154. if (!$dryRun) {
  155. $Model->save($record, ['validate' => false]);
  156. }
  157. $count++;
  158. }
  159. }
  160. $options['offset'] += 100;
  161. }
  162. return $count;
  163. }
  164. /**
  165. * Run before a model is saved
  166. *
  167. * @param Model $Model Model about to be saved.
  168. * @return bool true if save should proceed, false otherwise
  169. */
  170. public function process(Model $Model, $return = true) {
  171. foreach ($this->settings[$Model->alias]['fields'] as $field) {
  172. if (!empty($Model->data[$Model->alias][$field])) {
  173. $Model->data[$Model->alias][$field] = $this->_prepareInput($Model, $Model->data[$Model->alias][$field]);
  174. }
  175. }
  176. return $return;
  177. }
  178. /**
  179. * @param string $input
  180. * @return string cleanedInput
  181. */
  182. protected function _prepareInput(Model $Model, $string) {
  183. $map = $this->_map['in'];
  184. if ($this->settings[$Model->alias]['mergeQuotes']) {
  185. foreach ($map as $key => $val) {
  186. $map[$key] = $this->settings[$Model->alias]['mergeQuotes'];
  187. }
  188. }
  189. return str_replace(array_keys($map), array_values($map), $string);
  190. }
  191. }