PageRenderTime 50ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

/php-lib/core.php

http://github.com/facebook/xhp
PHP | 767 lines | 506 code | 87 blank | 174 comment | 78 complexity | ab2dfa8087fdc12192729ec14692740f MD5 | raw file
Possible License(s): MIT, MPL-2.0-no-copyleft-exception
  1. <?php
  2. /*
  3. +----------------------------------------------------------------------+
  4. | XHP |
  5. +----------------------------------------------------------------------+
  6. | Copyright (c) 2009 - 2010 Facebook, Inc. (http://www.facebook.com) |
  7. +----------------------------------------------------------------------+
  8. | This source file is subject to version 3.01 of the PHP license, |
  9. | that is bundled with this package in the file LICENSE.PHP, and is |
  10. | available through the world-wide-web at the following url: |
  11. | http://www.php.net/license/3_01.txt |
  12. | If you did not receive a copy of the PHP license and are unable to |
  13. | obtain it through the world-wide-web, please send a note to |
  14. | license@php.net so we can mail you a copy immediately. |
  15. +----------------------------------------------------------------------+
  16. */
  17. abstract class :x:base {
  18. abstract public function __construct();
  19. abstract public function appendChild($child);
  20. abstract public function getAttribute($attr);
  21. abstract public function setAttribute($attr, $val);
  22. abstract public function categoryOf($cat);
  23. abstract public function __toString();
  24. abstract protected static function &__xhpAttributeDeclaration();
  25. abstract protected function &__xhpCategoryDeclaration();
  26. abstract protected function &__xhpChildrenDeclaration();
  27. /**
  28. * Enabling validation will give you stricter documents; you won't be able to
  29. * do many things that violate the XHTML 1.0 Strict spec. It is recommend that
  30. * you leave this on because otherwise things like the `children` keyword will
  31. * do nothing. This validation comes at some CPU cost, however, so if you are
  32. * running a high-traffic site you will probably want to disable this in
  33. * production. You should still leave it on while developing new features,
  34. * though.
  35. */
  36. public static $ENABLE_VALIDATION = true;
  37. final protected static function renderChild($child) {
  38. if ($child instanceof :x:base) {
  39. return $child->__toString();
  40. } else if ($child instanceof HTML) {
  41. return $child->render();
  42. } else if (is_array($child)) {
  43. throw new XHPRenderArrayException('Can not render array!');
  44. } else {
  45. return htmlspecialchars((string)$child);
  46. }
  47. }
  48. public static function element2class($element) {
  49. return 'xhp_'.str_replace(array(':', '-'), array('__', '_'), $element);
  50. }
  51. public static function class2element($class) {
  52. return str_replace(array('__', '_'), array(':', '-'), preg_replace('#^xhp_#i', '', $class));
  53. }
  54. }
  55. abstract class :x:composable-element extends :x:base {
  56. private
  57. $attributes,
  58. $children;
  59. // Private constants indicating the declared types of attributes
  60. const TYPE_STRING = 1;
  61. const TYPE_BOOL = 2;
  62. const TYPE_NUMBER = 3;
  63. const TYPE_ARRAY = 4;
  64. const TYPE_OBJECT = 5;
  65. const TYPE_VAR = 6;
  66. const TYPE_ENUM = 7;
  67. const TYPE_FLOAT = 8;
  68. protected function init() {}
  69. /**
  70. * A new :x:composable-element is instantiated for every literal tag
  71. * expression in the script.
  72. *
  73. * The following code:
  74. * $foo = <foo attr="val">bar</foo>;
  75. *
  76. * will execute something like:
  77. * $foo = new xhp_foo(array('attr' => 'val'), array('bar'));
  78. *
  79. * @param $attributes map of attributes to values
  80. * @param $children list of children
  81. */
  82. final public function __construct($attributes, $children) {
  83. if ($attributes) {
  84. foreach ($attributes as $key => &$val) {
  85. $this->validateAttributeValue($key, $val);
  86. }
  87. unset($val);
  88. }
  89. $this->attributes = $attributes;
  90. $this->children = array();
  91. foreach ($children as $child) {
  92. $this->appendChild($child);
  93. }
  94. if (:x:base::$ENABLE_VALIDATION) {
  95. // There is some cost to having defaulted unused arguments on a function
  96. // so we leave these out and get them with func_get_args().
  97. $args = func_get_args();
  98. if (isset($args[2])) {
  99. $this->source = "$args[2]:$args[3]";
  100. } else {
  101. $this->source =
  102. 'You have ENABLE_VALIDATION on, but debug information is not being ' .
  103. 'passed to XHP objects correctly. Ensure xhp.include_debug is on ' .
  104. 'in your PHP configuration. Without this option enabled, ' .
  105. 'validation errors will be painful to debug at best.';
  106. }
  107. }
  108. $this->init();
  109. }
  110. /**
  111. * Adds a child to the end of this node. If you give an array to this method
  112. * then it will behave like a DocumentFragment.
  113. *
  114. * @param $child single child or array of children
  115. */
  116. final public function appendChild($child) {
  117. if (is_array($child)) {
  118. foreach ($child as $c) {
  119. $this->appendChild($c);
  120. }
  121. } else if ($child instanceof :x:frag) {
  122. $this->children = array_merge($this->children, $child->children);
  123. } else if ($child !== null) {
  124. $this->children[] = $child;
  125. }
  126. return $this;
  127. }
  128. /**
  129. * Fetches all direct children of this element that match a particular tag
  130. * name (or all children if no tag is given)
  131. *
  132. * @param $tag_name tag name (optional)
  133. * @return array
  134. */
  135. final protected function getChildren($tag_name = null) {
  136. if (!$tag_name) {
  137. return $this->children;
  138. }
  139. $tag_name = :x:base::element2class($tag_name);
  140. $ret = array();
  141. foreach ($this->children as $child) {
  142. if ($child instanceof $tag_name) {
  143. $ret[] = $child;
  144. }
  145. }
  146. return $ret;
  147. }
  148. /**
  149. * Fetches an attribute from this elements attribute store. If $attr is not
  150. * defined in the store, and $default is null an exception will be thrown. An
  151. * exception will also be thrown if $attr is not supported -- see
  152. * `supportedAttributes`
  153. *
  154. * @param $attr attribute to fetch
  155. * @return value
  156. */
  157. final public function getAttribute($attr) {
  158. // Return attribute if it's there, otherwise default or exception.
  159. if (isset($this->attributes[$attr])) {
  160. return $this->attributes[$attr];
  161. }
  162. // Get the declaration on miss
  163. $decl = $this->__xhpAttributeDeclaration();
  164. if (!isset($decl[$attr])) {
  165. throw new XHPAttributeNotSupportedException($this, $attr);
  166. } else if (!empty($decl[$attr][3])) {
  167. throw new XHPAttributeRequiredException($this, $attr);
  168. } else {
  169. $decl = $this->__xhpAttributeDeclaration();
  170. return $decl[$attr][2];
  171. }
  172. }
  173. final protected function getAttributes() {
  174. return $this->attributes;
  175. }
  176. /**
  177. * Sets an attribute in this element's attribute store. An exception will be
  178. * thrown if $attr is not supported -- see `supportedAttributes`.
  179. *
  180. * @param $attr attribute to set
  181. * @param $val value
  182. */
  183. final public function setAttribute($attr, $val) {
  184. $this->validateAttributeValue($attr, $val);
  185. $this->attributes[$attr] = $val;
  186. return $this;
  187. }
  188. final protected function __flushElementChildren() {
  189. // Flush all :x:base elements to x:primitive's
  190. $ln = count($this->children);
  191. for ($ii = 0; $ii < $ln; ++$ii) {
  192. $child = $this->children[$ii];
  193. if ($child instanceof :x:element) {
  194. do {
  195. if (:x:base::$ENABLE_VALIDATION) {
  196. $child->validateChildren();
  197. }
  198. $child = $child->render();
  199. } while ($child instanceof :x:element);
  200. if (!($child instanceof :x:primitive)) {
  201. throw new XHPCoreRenderException($this->children[$ii], $child);
  202. }
  203. if ($child instanceof :x:frag) {
  204. array_splice($this->children, $ii, 1, $child->children);
  205. $ln = count($this->children);
  206. --$ii;
  207. } else {
  208. $this->children[$ii] = $child;
  209. }
  210. }
  211. }
  212. }
  213. /**
  214. * Defined in elements by the `attribute` keyword. The declaration is simple.
  215. * There is a keyed array, with each key being an attribute. Each value is
  216. * an array with 4 elements. The first is the attribute type. The second is
  217. * meta-data about the attribute. The third is a default value (null for
  218. * none). And the fourth is whether or not this value is required.
  219. *
  220. * Attribute types are suggested by the TYPE_* constants.
  221. */
  222. protected static function &__xhpAttributeDeclaration() {
  223. static $_ = array();
  224. return $_;
  225. }
  226. /**
  227. * Defined in elements by the `category` keyword. This is just a list of all
  228. * categories an element belongs to. Each category is a key with value 1.
  229. */
  230. protected function &__xhpCategoryDeclaration() {
  231. static $_ = array();
  232. return $_;
  233. }
  234. /**
  235. * Defined in elements by the `children` keyword. This returns a pattern of
  236. * allowed children. The return value is potentially very complicated. The
  237. * two simplest are 0 and 1 which mean no children and any children,
  238. * respectively. Otherwise you're dealing with an array which is just the
  239. * biggest mess you've ever seen.
  240. */
  241. protected function &__xhpChildrenDeclaration() {
  242. static $_ = 1;
  243. return $_;
  244. }
  245. /**
  246. * Throws an exception if $val is not a valid value for the attribute $attr
  247. * on this element.
  248. */
  249. final protected function validateAttributeValue($attr, &$val) {
  250. $decl = $this->__xhpAttributeDeclaration();
  251. if (!isset($decl[$attr])) {
  252. throw new XHPAttributeNotSupportedException($this, $attr);
  253. }
  254. if ($val === null) {
  255. return;
  256. }
  257. switch ($decl[$attr][0]) {
  258. case self::TYPE_STRING:
  259. $val = (string)$val;
  260. return;
  261. case self::TYPE_BOOL:
  262. if (!is_bool($val)) {
  263. if ($val === "false") {
  264. $val = false;
  265. } else {
  266. $val = (bool)$val;
  267. }
  268. }
  269. return;
  270. case self::TYPE_NUMBER:
  271. if (!is_int($val)) {
  272. $val = (int)$val;
  273. }
  274. return;
  275. case self::TYPE_FLOAT:
  276. if (!is_numeric($val)) {
  277. $val = (float)$val;
  278. }
  279. return;
  280. case self::TYPE_ARRAY:
  281. if (!is_array($val)) {
  282. throw new XHPInvalidAttributeException($this, 'array', $attr, $val);
  283. }
  284. return;
  285. case self::TYPE_OBJECT:
  286. if (!($val instanceof $decl[$attr][1])) {
  287. throw new XHPInvalidAttributeException(
  288. $this, $decl[$attr][1], $attr, $val
  289. );
  290. }
  291. return;
  292. // case self::TYPE_VAR: `var` (any type)
  293. case self::TYPE_ENUM:
  294. foreach ($decl[$attr][1] as $enum) {
  295. if ($enum === $val) {
  296. return;
  297. }
  298. }
  299. $enums = 'enum("' . implode('","', $decl[$attr][1]) . '")';
  300. throw new XHPInvalidAttributeException($this, $enums, $attr, $val);
  301. }
  302. }
  303. /**
  304. * Validates that this element's children match its children descriptor, and
  305. * throws an exception if that's not the case.
  306. */
  307. final protected function validateChildren() {
  308. $decl = $this->__xhpChildrenDeclaration();
  309. if ($decl === 1) { // Any children allowed
  310. return;
  311. }
  312. if ($decl === 0) { // No children allowed
  313. if ($this->children) {
  314. throw new XHPInvalidChildrenException($this, 0);
  315. } else {
  316. return;
  317. }
  318. }
  319. $ii = 0;
  320. if (!$this->validateChildrenExpression($decl, $ii) ||
  321. $ii < count($this->children)) {
  322. // Use of HTML() breaks the content model definition.
  323. // Lesson: Don't use HTML().
  324. if (isset($this->children[$ii]) && $this->children[$ii] instanceof HTML) {
  325. return;
  326. }
  327. throw new XHPInvalidChildrenException($this, $ii);
  328. }
  329. }
  330. final private function validateChildrenExpression($decl, &$index) {
  331. switch ($decl[0]) {
  332. case 0: // Exactly once -- :fb-thing
  333. if ($this->validateChildrenRule($decl[1], $decl[2], $index)) {
  334. return true;
  335. }
  336. return false;
  337. case 1: // Zero or more times -- :fb-thing*
  338. while ($this->validateChildrenRule($decl[1], $decl[2], $index));
  339. return true;
  340. case 2: // Zero or one times -- :fb-thing?
  341. if ($this->validateChildrenRule($decl[1], $decl[2], $index));
  342. return true;
  343. case 3: // One or more times -- :fb-thing+
  344. if (!$this->validateChildrenRule($decl[1], $decl[2], $index)) {
  345. return false;
  346. }
  347. while ($this->validateChildrenRule($decl[1], $decl[2], $index));
  348. return true;
  349. case 4: // Specific order -- :fb-thing, :fb-other-thing
  350. $oindex = $index;
  351. if ($this->validateChildrenExpression($decl[1], $index) &&
  352. $this->validateChildrenExpression($decl[2], $index)) {
  353. return true;
  354. }
  355. $index = $oindex;
  356. return false;
  357. case 5: // Either or -- :fb-thing | :fb-other-thing
  358. if ($this->validateChildrenExpression($decl[1], $index) ||
  359. $this->validateChildrenExpression($decl[2], $index)) {
  360. return true;
  361. }
  362. return false;
  363. }
  364. }
  365. final private function validateChildrenRule($type, $rule, &$index) {
  366. switch ($type) {
  367. case 1: // any element -- any
  368. if (isset($this->children[$index])) {
  369. ++$index;
  370. return true;
  371. }
  372. return false;
  373. case 2: // pcdata -- pcdata
  374. if (isset($this->children[$index]) &&
  375. !($this->children[$index] instanceof :x:base)) {
  376. ++$index;
  377. return true;
  378. }
  379. return false;
  380. case 3: // specific element -- :fb-thing
  381. if (isset($this->children[$index]) &&
  382. $this->children[$index] instanceof $rule) {
  383. ++$index;
  384. return true;
  385. }
  386. return false;
  387. case 4: // element category -- %block
  388. if (!isset($this->children[$index]) ||
  389. !($this->children[$index] instanceof :x:base)) {
  390. return false;
  391. }
  392. $categories = $this->children[$index]->__xhpCategoryDeclaration();
  393. if (empty($categories[$rule])) {
  394. return false;
  395. }
  396. ++$index;
  397. return true;
  398. case 5: // nested rule -- ((:fb-thing, :fb-other-thing)*, :fb:thing-footer)
  399. return $this->validateChildrenExpression($rule, $index);
  400. }
  401. }
  402. /**
  403. * Returns the human-readable `children` declaration as seen in this class's
  404. * source code.
  405. */
  406. final public function __getChildrenDeclaration() {
  407. $decl = $this->__xhpChildrenDeclaration();
  408. if ($decl === 1) {
  409. return 'any';
  410. }
  411. if ($decl === 0) {
  412. return 'empty';
  413. }
  414. return $this->renderChildrenDeclaration($decl);
  415. }
  416. final private function renderChildrenDeclaration($decl) {
  417. switch ($decl[0]) {
  418. case 0:
  419. return $this->renderChildrenRule($decl[1], $decl[2]);
  420. case 1:
  421. return $this->renderChildrenRule($decl[1], $decl[2]) . '*';
  422. case 2:
  423. return $this->renderChildrenRule($decl[1], $decl[2]) . '?';
  424. case 3:
  425. return $this->renderChildrenRule($decl[1], $decl[2]) . '+';
  426. case 4:
  427. return $this->renderChildrenDeclaration($decl[1]) . ',' .
  428. $this->renderChildrenDeclaration($decl[2]);
  429. case 5:
  430. return $this->renderChildrenDeclaration($decl[1]) . '|' .
  431. $this->renderChildrenDeclaration($decl[2]);
  432. }
  433. }
  434. final private function renderChildrenRule($type, $rule) {
  435. switch ($type) {
  436. case 1:
  437. return 'any';
  438. case 2:
  439. return 'pcdata';
  440. case 3:
  441. return ':' . :x:base::class2element($rule);
  442. case 4:
  443. return '%' . $rule;
  444. case 5:
  445. return '(' . $this->renderChildrenDeclaration($rule) . ')';
  446. }
  447. }
  448. /**
  449. * Returns a description of the current children in this element. Maybe
  450. * something like this:
  451. * <div><span>foo</span>bar</div> ->
  452. * :span[%inline],pcdata
  453. */
  454. final public function __getChildrenDescription() {
  455. $desc = array();
  456. foreach ($this->children as $child) {
  457. if ($child instanceof :x:base) {
  458. $tmp = ':' . :x:base::class2element(get_class($child));
  459. if ($categories = $child->__xhpCategoryDeclaration()) {
  460. $tmp .= '[%'. implode(',%', array_keys($categories)) . ']';
  461. }
  462. $desc[] = $tmp;
  463. } else {
  464. $desc[] = 'pcdata';
  465. }
  466. }
  467. return implode(',', $desc);
  468. }
  469. final public function categoryOf($c) {
  470. $categories = $this->__xhpCategoryDeclaration();
  471. return isset($categories[$c]);
  472. }
  473. }
  474. /**
  475. * :x:primitive lays down the foundation for very low-level elements. You
  476. * should directly :x:primitive only if you are creating a core element that
  477. * needs to directly implement stringify(). All other elements should subclass
  478. * from :x:element.
  479. */
  480. abstract class :x:primitive extends :x:composable-element {
  481. abstract protected function stringify();
  482. /**
  483. * This isn't __toString() because throwing an exception out of __toString()
  484. * produces a useless, immediate fatal, and allowing XHP to seamlessly cast
  485. * into strings encourages bad practices, like this real snippet:
  486. *
  487. * $links .= <a>...</a>;
  488. * $links .= <a>...</a>;
  489. * return HTML($links);
  490. *
  491. */
  492. final public function __toString() {
  493. // Validate our children
  494. $this->__flushElementChildren();
  495. if (:x:base::$ENABLE_VALIDATION) {
  496. $this->validateChildren();
  497. }
  498. // Render to string
  499. return $this->stringify();
  500. }
  501. }
  502. /**
  503. * :x:element defines an interface that all user-land elements should subclass
  504. * from. The main difference between :x:element and :x:primitive is that
  505. * subclasses of :x:element should implement `render()` instead of `stringify`.
  506. * This is important because most elements should not be dealing with strings
  507. * of markup.
  508. */
  509. abstract class :x:element extends :x:composable-element {
  510. final public function __toString() {
  511. $that = $this;
  512. if (:x:base::$ENABLE_VALIDATION) {
  513. // Validate the current object
  514. $that->validateChildren();
  515. // And each intermediary object it returns
  516. while (($that = $that->render()) instanceof :x:element) {
  517. $that->validateChildren();
  518. }
  519. // render() must always return XHPPrimitives
  520. if (!($that instanceof :x:composable-element)) {
  521. throw new XHPCoreRenderException($this, $that);
  522. }
  523. } else {
  524. // Skip the above checks when not validating
  525. while (($that = $that->render()) instanceof :x:element);
  526. }
  527. return $that->__toString();
  528. }
  529. }
  530. /**
  531. * :x:composite is a special class which allows you to pass around a node that
  532. * acts on another node, but any `appendChild()` calls will append to one of its
  533. * children.
  534. *
  535. * This can be useful if you want to pass around an object that will later be
  536. * wrapped on the inside.
  537. *
  538. * For instance, you can define an :x:composite like this:
  539. * $parent = <div><p>{$anchor = <span />}</p></div>;
  540. * $composite = new :x:composite($parent, $anchor);
  541. *
  542. * Then if another client wants to wrap the contents of your composite node,
  543. * he would do so by:
  544. * $composite->appendChild($anchor = <b />);
  545. * $composite = new :x:composite($composite, $anchor);
  546. *
  547. * Note that we create another composite from the old one so that later down the
  548. * line we can wrap again with another tag.
  549. *
  550. * IMPORTANT: I think this class is broken right now. If you want to use it you
  551. * should try to fix it.
  552. */
  553. class :x:composite extends :x:base {
  554. private
  555. $parent,
  556. $anchor;
  557. public function __construct(:x:base $parent, :x:base $anchor) {
  558. $this->parent = $parent;
  559. $this->anchor = $anchor;
  560. }
  561. public function appendChild($child) {
  562. return $this->anchor->appendChild($child);
  563. }
  564. public function getAttribute($attr) {
  565. return $this->parent->getAttribute($attr);
  566. }
  567. public function setAttribute($attr, $val) {
  568. return $this->parent->setAttribute($attr, $val);
  569. }
  570. public function categoryOf($cat) {
  571. return $this->parent->categoryOf($cat);
  572. }
  573. public function __toString() {
  574. return $this->parent->__toString();
  575. }
  576. protected static function &__xhpAttributeDeclaration() {
  577. return $this->parent->__xhpAttributeDeclaration();
  578. }
  579. protected function &__xhpCategoryDeclaration() {
  580. return $this->parent->__xhpCategoryDeclaration();
  581. }
  582. protected function &__xhpChildrenDeclaration() {
  583. return $this->parent->__xhpChildrenDeclaration();
  584. }
  585. }
  586. /**
  587. * An <x:frag /> is a transparent wrapper around any number of elements. When
  588. * you render it just the children will be rendered. When you append it to an
  589. * element the <x:frag /> will disappear and each child will be sequentially
  590. * appended to the element.
  591. */
  592. class :x:frag extends :x:primitive {
  593. protected function stringify() {
  594. $buf = '';
  595. foreach ($this->getChildren() as $child) {
  596. $buf .= :x:base::renderChild($child);
  597. }
  598. return $buf;
  599. }
  600. }
  601. /**
  602. * Exceptions are neat.
  603. */
  604. class XHPException extends Exception {
  605. protected static function getElementName($that) {
  606. $name = get_class($that);
  607. if (substr($name, 0, 4) !== 'xhp_') {
  608. return $name;
  609. } else {
  610. return :x:base::class2element($name);
  611. }
  612. }
  613. }
  614. class XHPClassException extends XHPException {
  615. public function __construct($that, $msg) {
  616. parent::__construct(
  617. 'Exception in class `' . XHPException::getElementName($that) . "`\n\n".
  618. "$that->source\n\n".
  619. $msg
  620. );
  621. }
  622. }
  623. class XHPCoreRenderException extends XHPException {
  624. public function __construct($that, $rend) {
  625. parent::__construct(
  626. ':x:element::render must reduce an object to an :x:primitive, but `'.
  627. :x:base::class2element(get_class($that)).'` reduced into `'.gettype($rend)."`.\n\n".
  628. $that->source
  629. );
  630. }
  631. }
  632. class XHPRenderArrayException extends XHPException {
  633. }
  634. class XHPAttributeNotSupportedException extends XHPException {
  635. public function __construct($that, $attr) {
  636. parent::__construct(
  637. 'Attribute `'.$attr.'` is not supported in class '.
  638. '`'.XHPException::getElementName($that)."`.\n\n".
  639. "$that->source\n\n".
  640. 'Please check for typos in your attribute. If you are creating a new '.
  641. 'attribute on this element please add your attribute to the '.
  642. "`supportedAttributes` method.\n\n"
  643. );
  644. }
  645. }
  646. class XHPAttributeRequiredException extends XHPException {
  647. public function __construct($that, $attr) {
  648. parent::__construct(
  649. 'Required attribute `'.$attr.'` was not specified in element '.
  650. '`'.XHPException::getElementName($that)."`.\n\n".
  651. $that->source
  652. );
  653. }
  654. }
  655. class XHPInvalidAttributeException extends XHPException {
  656. public function __construct($that, $type, $attr, $val) {
  657. if (is_object($val)) {
  658. $val_type = get_class($val);
  659. } else {
  660. $val_type = gettype($val);
  661. }
  662. parent::__construct(
  663. "Invalid attribute `$attr` of type `$val_type` supplied to element `".
  664. :x:base::class2element(get_class($that))."`, expected `$type`.\n\n".
  665. $that->source
  666. );
  667. }
  668. }
  669. class XHPInvalidChildrenException extends XHPException {
  670. public function __construct($that, $index) {
  671. parent::__construct(
  672. 'Element `'.XHPException::getElementName($that).'` was rendered with '.
  673. "invalid children.\n\n".
  674. "$that->source\n\n".
  675. "Verified $index children before failing.\n\n".
  676. "Children expected:\n".$that->__getChildrenDeclaration()."\n\n".
  677. "Children received:\n".$that->__getChildrenDescription()
  678. );
  679. }
  680. }