Erebot  latest
A modular IRC bot for PHP 5.3+
Styling.php
1 <?php
2 /*
3  This file is part of Erebot, a modular IRC bot written in PHP.
4 
5  Copyright © 2010 François Poirotte
6 
7  Erebot is free software: you can redistribute it and/or modify
8  it under the terms of the GNU General Public License as published by
9  the Free Software Foundation, either version 3 of the License, or
10  (at your option) any later version.
11 
12  Erebot is distributed in the hope that it will be useful,
13  but WITHOUT ANY WARRANTY; without even the implied warranty of
14  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15  GNU General Public License for more details.
16 
17  You should have received a copy of the GNU General Public License
18  along with Erebot. If not, see <http://www.gnu.org/licenses/>.
19 */
20 
21 namespace Erebot;
22 
114 {
116  protected $translator;
117 
119  protected $cls;
120 
128  public function __construct(\Erebot\IntlInterface $translator)
129  {
130  $this->translator = $translator;
131  $this->cls = array(
132  'int' => '\\Erebot\\Styling\\Variables\\IntegerVariable',
133  'float' => '\\Erebot\\Styling\\Variables\\FloatVariable',
134  'string' => '\\Erebot\\Styling\\Variables\\StringVariable',
135  );
136  }
137 
150  public function getClass($type)
151  {
152  if (!isset($this->cls[$type])) {
153  throw new \InvalidArgumentException('Invalid type');
154  }
155  return $this->cls[$type];
156  }
157 
170  public function setClass($type, $cls)
171  {
172  if (!isset($this->cls[$type])) {
173  throw new \InvalidArgumentException('Invalid type');
174  }
175  if (!is_string($cls)) {
176  throw new \InvalidArgumentException(
177  'Expected a string for the class'
178  );
179  }
180  if (!class_exists($cls)) {
181  throw new \InvalidArgumentException('Class not found');
182  }
183  if (!($cls instanceof \Erebot\Styling\VariableInterface)) {
184  throw new \InvalidArgumentException(
185  'Must be a subclass of \\Erebot\\Styling\\VariableInterface'
186  );
187  }
188  $this->cls[$type] = $cls;
189  }
190 
204  protected static function checkVariableName($var)
205  {
206  if (!preg_match('/^[a-zA-Z0-9_\.]+$/D', $var)) {
207  throw new \InvalidArgumentException(
208  'Invalid variable name "'.$var.'". '.
209  'Variable names may only contain alphanumeric '.
210  'characters, underscores ("_") and dots (".").'
211  );
212  }
213  }
214 
215  // @codingStandardsIgnoreStart
216  public function _($template, array $vars = array())
217  {
218  $source = $this->translator->_($template);
219  return $this->render($source, $vars);
220  // @codingStandardsIgnoreEnd
221  }
222 
223  public function render($template, array $vars = array())
224  {
225  // For basic strings that don't contain any markup,
226  // we try to be as efficient as possible.
227  if (strpos($template, '<') === false &&
228  strpos($template, '&') === false) {
229  return $template;
230  }
231 
232  $attributes = array(
233  'underline' => 0,
234  'bold' => 0,
235  'bg' => null,
236  'fg' => null,
237  );
238 
239  $variables = array();
240  foreach ($vars as $name => $var) {
241  $variables[$name] = $this->wrapScalar($var, $name);
242  }
243 
244  $dom = self::parseTemplate($template);
245  $result = $this->parseNode(
246  $dom->documentElement,
247  $attributes,
248  $variables
249  );
250 
251  $pattern = '@'.
252  '\\003,(?![01])'.
253  '|'.
254  '\\003(?:[0-9]{2})?,(?:[0-9]{2})?(?:\\002\\002)?(?=\\003)'.
255  '|'.
256  '(\\003(?:[0-9]{2})?,)\\002\\002(?![0-9])'.
257  '|'.
258  '(\\003[0-9]{2})\\002\\002(?!,)'.
259  '@';
260  $replace = '\\1\\2';
261  $result = preg_replace($pattern, $replace, $result);
262  return $result;
263  }
264 
265  public function getTranslator()
266  {
267  return $this->translator;
268  }
269 
304  protected function wrapScalar($var, $name)
305  {
306  self::checkVariableName($name);
307 
308  if (is_object($var)) {
309  if ($var instanceof \Erebot\Styling\VariableInterface) {
310  return $var;
311  }
312 
313  if (!is_callable(array($var, '__toString'), false)) {
314  throw new \InvalidArgumentException(
315  $name.' must be a scalar or an instance of '.
316  '\\Erebot\\Styling\\VariableInterface'
317  );
318  }
319  }
320 
321  if (is_array($var)) {
322  return $var;
323  }
324 
325  if (is_string($var) || is_callable(array($var, '__toString'), false)) {
326  $cls = $this->cls['string'];
327  } elseif (is_int($var)) {
328  $cls = $this->cls['int'];
329  } elseif (is_float($var)) {
330  $cls = $this->cls['float'];
331  } else {
332  throw new \InvalidArgumentException(
333  'Unsupported scalar type ('.gettype($var).') for "'.$name.'"'
334  );
335  }
336  return new $cls($var);
337  }
338 
353  protected static function parseTemplate($source)
354  {
355  $source =
356  '<msg xmlns="http://www.erebot.net/xmlns/erebot/styling">'.
357  $source.
358  '</msg>';
359  $schema = dirname(__DIR__) .
360  DIRECTORY_SEPARATOR . 'data' .
361  DIRECTORY_SEPARATOR . 'styling.rng';
362  $dom = new \Erebot\DOM();
363  $dom->substituteEntities = true;
364  $dom->resolveExternals = false;
365  $dom->recover = true;
366  $ue = libxml_use_internal_errors(true);
367  $dom->loadXML($source);
368  $valid = $dom->relaxNGValidate($schema);
369  $errors = $dom->getErrors();
370  libxml_use_internal_errors($ue);
371 
372  if (!$valid || count($errors)) {
373  // Some unpredicted error occurred,
374  // show some (hopefully) useful information.
375  if (class_exists('\\Plop')) {
376  $logger = \Plop::getInstance();
377  $logger->error(print_r($errors, true));
378  }
379  throw new \InvalidArgumentException(
380  'Error while validating the message'
381  );
382  }
383  return $dom;
384  }
385 
401  protected function parseNode($node, &$attributes, $vars)
402  {
403  $result = '';
404  $saved = $attributes;
405 
406  if ($node->nodeType == XML_TEXT_NODE) {
407  return $node->nodeValue;
408  }
409 
410  if ($node->nodeType != XML_ELEMENT_NODE) {
411  return '';
412  }
413 
414  // Pre-handling.
415  switch ($node->tagName) {
416  case 'var':
417  $lexer = new \Erebot\Styling\Lexer(
418  $node->getAttribute('name'),
419  $vars
420  );
421  $var = $lexer->getResult();
422  if (!($var instanceof \Erebot\Styling\VariableInterface)) {
423  return (string) $var;
424  }
425  return $var->render($this->translator);
426 
427  case 'u':
428  if (!$attributes['underline']) {
429  $result .= self::CODE_UNDERLINE;
430  }
431  $attributes['underline'] = 1;
432  break;
433 
434  case 'b':
435  if (!$attributes['bold']) {
436  $result .= self::CODE_BOLD;
437  }
438  $attributes['bold'] = 1;
439  break;
440 
441  case 'color':
442  $colors = array('', '');
443  $mapping = array('fg', 'bg');
444 
445  foreach ($mapping as $pos => $color) {
446  $value = $node->getAttribute($color);
447  if ($value != '') {
448  $value = str_replace(array(' ', '-'), '_', $value);
449  if (strspn($value, '1234567890') !== strlen($value)) {
450  $reflector = new \ReflectionClass('\\Erebot\\StylingInterface');
451  if (!$reflector->hasConstant('COLOR_'.strtoupper($value))) {
452  throw new \InvalidArgumentException(
453  'Invalid color "'.$value.'"'
454  );
455  }
456  $value = $reflector->getConstant('COLOR_'.strtoupper($value));
457  }
458  $attributes[$color] = sprintf('%02d', $value);
459  if ($attributes[$color] != $saved[$color]) {
460  $colors[$pos] = $attributes[$color];
461  }
462  }
463  }
464 
465  $code = implode(',', $colors);
466  if ($colors[0] != '' && $colors[1] != '') {
467  $result .= self::CODE_COLOR.$code;
468  } elseif ($code != ',') {
469  $result .= self::CODE_COLOR.rtrim($code, ',').
470  self::CODE_BOLD.self::CODE_BOLD;
471  }
472  break;
473  }
474 
475  if ($node->tagName == 'for') {
476  // Handle loops.
477  $savedVariables = $vars;
478  $separator = array(', ', ' & ');
479 
480  foreach (array('separator', 'sep') as $attr) {
481  $attrNode = $node->getAttributeNode($attr);
482  if ($attrNode !== false) {
483  $separator[0] = $separator[1] = $attrNode->nodeValue;
484  break;
485  }
486  }
487 
488  foreach (array('last_separator', 'last') as $attr) {
489  $attrNode = $node->getAttributeNode($attr);
490  if ($attrNode !== false) {
491  $separator[1] = $attrNode->nodeValue;
492  break;
493  }
494  }
495 
496  $loopKey = $node->getAttribute('key');
497  $loopItem = $node->getAttribute('item');
498  $loopFrom = $node->getAttribute('from');
499  $count = count($vars[$loopFrom]);
500  reset($vars[$loopFrom]);
501 
502  for ($i = 1; $i < $count; $i++) {
503  if ($i > 1) {
504  $result .= $separator[0];
505  }
506 
507  $key = key($vars[$loopFrom]);
508  next($vars[$loopFrom]);
509  if ($loopKey !== null) {
510  $cls = $this->cls['string'];
511  $vars[$loopKey] = new $cls($key);
512  }
513  $vars[$loopItem] = $this->wrapScalar(
514  $vars[$loopFrom][$key],
515  $loopItem
516  );
517 
518  $result .= $this->parseChildren(
519  $node,
520  $attributes,
521  $vars
522  );
523  }
524 
525  $key = key($vars[$loopFrom]);
526  if ($key === null) {
527  $item = array('key' => '', 'value' => '');
528  } else {
529  $item = array('key' => $key, 'value' => $vars[$loopFrom][$key]);
530  }
531 
532  if ($loopKey !== null) {
533  $cls = $this->cls['string'];
534  $vars[$loopKey] = new $cls($item['key']);
535  }
536 
537  $vars[$loopItem] = $this->wrapScalar($item['value'], $loopItem);
538  if ($count > 1) {
539  $result .= $separator[1];
540  }
541 
542  $result .= $this->parseChildren($node, $attributes, $vars);
543  $vars = $savedVariables;
544  } elseif ($node->tagName == 'plural') {
545  // Handle plurals.
546  /* We don't need the full set of features/complexity/bugs
547  * ICU contains. Here, we use a simple "plural" formatter
548  * to detect the right plural form to use. The formatting
549  * steps are done without relying on ICU. */
550  $attrNode = $node->getAttributeNode('var');
551  if ($attrNode === false) {
552  throw new \InvalidArgumentException(
553  'No variable name given'
554  );
555  }
556 
557  $lexer = new \Erebot\Styling\Lexer($attrNode->nodeValue, $vars);
558  $value = $lexer->getResult();
559  if ($value instanceof \Erebot\Styling\VariableInterface) {
560  $value = $value->getValue();
561  }
562  $value = (int) $value;
563 
564  $subcontents = array();
565  $pattern = '{0,plural,';
566  for ($child = $node->firstChild; $child != null; $child = $child->nextSibling) {
567  if ($child->nodeType != XML_ELEMENT_NODE ||
568  $child->tagName != 'case') {
569  continue;
570  }
571 
572  // See this class documentation for a link
573  // which lists available forms for each language.
574  $form = $child->getAttribute('form');
575  $subcontents[$form] = $this->parseNode($child, $attributes, $vars);
576  $pattern .= $form.'{'.$form.'} ';
577  }
578  $pattern .= '}';
579  $locale = $this->translator->getLocale(
580  \Erebot\IntlInterface::LC_MESSAGES
581  );
582  $formatter = new \MessageFormatter($locale, $pattern);
583  // HACK: PHP <= 5.3.3 returns null when the pattern in invalid
584  // instead of throwing an exception.
585  // See http://bugs.php.net/bug.php?id=52776
586  if ($formatter === null) {
587  throw new \InvalidArgumentException('Invalid plural forms');
588  }
589  $correctForm = $formatter->format(array($value));
590  $result .= $subcontents[$correctForm];
591  } else {
592  // Handle children.
593  $result .= $this->parseChildren($node, $attributes, $vars);
594  }
595 
596  // Post-handling : restore old state.
597  switch ($node->tagName) {
598  case 'u':
599  if (!$saved['underline']) {
600  $result .= self::CODE_UNDERLINE;
601  }
602  $attributes['underline'] = 0;
603  break;
604 
605  case 'b':
606  if (!$saved['bold']) {
607  $result .= self::CODE_BOLD;
608  }
609  $attributes['bold'] = 0;
610  break;
611 
612  case 'color':
613  $colors = array('', '');
614  $mapping = array('fg', 'bg');
615 
616  foreach ($mapping as $pos => $color) {
617  if ($attributes[$color] != $saved[$color]) {
618  $colors[$pos] = $saved[$color];
619  }
620  $attributes[$color] = $saved[$color];
621  }
622 
623  $code = implode(',', $colors);
624  if ($colors[0] != '' && $colors[1] != '') {
625  $result .= self::CODE_COLOR.$code;
626  } elseif ($code != ',') {
627  $result .= self::CODE_COLOR.rtrim($code, ',').
628  self::CODE_BOLD.self::CODE_BOLD;
629  }
630  break;
631  }
632 
633  return $result;
634  }
635 
652  private function parseChildren($node, &$attributes, $vars)
653  {
654  $result = '';
655  for ($child = $node->firstChild; $child != null; $child = $child->nextSibling) {
656  $result .= $this->parseNode($child, $attributes, $vars);
657  }
658  return $result;
659  }
660 }
Provides styling (formatting) features.
Definition: Styling.php:113
Interface to provide internationalization.
Definition: CLI.php:21
_($template, array $vars=array())
Definition: Styling.php:216
parseNode($node, &$attributes, $vars)
Definition: Styling.php:401
setClass($type, $cls)
Definition: Styling.php:170
render($template, array $vars=array())
Definition: Styling.php:223
static parseTemplate($source)
Definition: Styling.php:353
Interface for styling (formatting) capabilities.
static checkVariableName($var)
Definition: Styling.php:204
wrapScalar($var, $name)
Definition: Styling.php:304
$cls
Maps some scalar types to a typed variable.
Definition: Styling.php:119
getClass($type)
Definition: Styling.php:150
$translator
Translator to use to improve rendering.
Definition: Styling.php:116
parseChildren($node, &$attributes, $vars)
Definition: Styling.php:652
__construct(\Erebot\IntlInterface $translator)
Definition: Styling.php:128