PageRenderTime 52ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 0ms

/src/core/ComposableElement.php

http://github.com/facebook/xhp
PHP | 766 lines | 504 code | 56 blank | 206 comment | 93 complexity | 30cc8bc05212b8cebc31d7bf7aae5a38 MD5 | raw file
Possible License(s): MIT, MPL-2.0-no-copyleft-exception
  1. <?hh
  2. /*
  3. * Copyright (c) 2015, Facebook, Inc.
  4. * All rights reserved.
  5. *
  6. * This source code is licensed under the BSD-style license found in the
  7. * LICENSE file in the root directory of this source tree. An additional grant
  8. * of patent rights can be found in the PATENTS file in the same directory.
  9. *
  10. */
  11. // Composer didn't support autoloading enums until recently (2015-03-09)
  12. require_once('ReflectionXHPAttribute.php');
  13. require_once('ReflectionXHPChildrenDeclaration.php');
  14. abstract class :x:composable-element extends :xhp {
  15. private Map<string, mixed> $attributes = Map {};
  16. private Vector<XHPChild> $children = Vector {};
  17. private Map<string, mixed> $context = Map {};
  18. protected function init(): void {}
  19. /**
  20. * A new :x:composable-element is instantiated for every literal tag
  21. * expression in the script.
  22. *
  23. * The following code:
  24. * $foo = <foo attr="val">bar</foo>;
  25. *
  26. * will execute something like:
  27. * $foo = new xhp_foo(array('attr' => 'val'), array('bar'));
  28. *
  29. * @param $attributes map of attributes to values
  30. * @param $children list of children
  31. */
  32. final public function __construct(KeyedTraversable<string, mixed> $attributes,
  33. Traversable<XHPChild> $children) {
  34. parent::__construct($attributes, $children);
  35. foreach ($children as $child) {
  36. $this->appendChild($child);
  37. }
  38. $this->setAttributes($attributes);
  39. if (:xhp::$ENABLE_VALIDATION) {
  40. // There is some cost to having defaulted unused arguments on a function
  41. // so we leave these out and get them with func_get_args().
  42. $args = func_get_args();
  43. if (isset($args[2])) {
  44. $this->source = "$args[2]:$args[3]";
  45. } else {
  46. $this->source =
  47. 'You have ENABLE_VALIDATION on, but debug information is not being ' .
  48. 'passed to XHP objects correctly. Ensure xhp.include_debug is on ' .
  49. 'in your PHP configuration. Without this option enabled, ' .
  50. 'validation errors will be painful to debug at best.';
  51. }
  52. }
  53. $this->init();
  54. }
  55. /**
  56. * Adds a child to the end of this node. If you give an array to this method
  57. * then it will behave like a DocumentFragment.
  58. *
  59. * @param $child single child or array of children
  60. */
  61. final public function appendChild(mixed $child): this {
  62. if ($child instanceof Traversable) {
  63. foreach ($child as $c) {
  64. $this->appendChild($c);
  65. }
  66. } else if ($child instanceof :x:frag) {
  67. $this->children->addAll($child->getChildren());
  68. } else if ($child !== null) {
  69. assert($child instanceof XHPChild);
  70. $this->children->add($child);
  71. }
  72. return $this;
  73. }
  74. /**
  75. * Adds a child to the beginning of this node. If you give an array to this
  76. * method then it will behave like a DocumentFragment.
  77. *
  78. * @param $child single child or array of children
  79. */
  80. final public function prependChild(mixed $child): this {
  81. // There's no prepend to a Vector, so reverse, append, and reverse agains
  82. $this->children->reverse();
  83. $this->appendChild($child);
  84. $this->children->reverse();
  85. return $this;
  86. }
  87. /**
  88. * Replaces all children in this node. You may pass a single array or
  89. * multiple parameters.
  90. *
  91. * @param $children Single child or array of children
  92. */
  93. final public function replaceChildren(...): this {
  94. // This function has been micro-optimized
  95. $args = func_get_args();
  96. $new_children = Vector {};
  97. foreach ($args as $xhp) {
  98. if ($xhp) {
  99. if ($xhp instanceof :x:frag) {
  100. foreach ($xhp->children as $child) {
  101. $new_children->add($child);
  102. }
  103. } else if (!($xhp instanceof Traversable)) {
  104. $new_children->add($xhp);
  105. } else {
  106. foreach ($xhp as $element) {
  107. if ($element instanceof :x:frag) {
  108. foreach ($element->children as $child) {
  109. $new_children->add($child);
  110. }
  111. } else if ($element !== null) {
  112. $new_children->add($element);
  113. }
  114. }
  115. }
  116. }
  117. }
  118. $this->children = $new_children;
  119. return $this;
  120. }
  121. /**
  122. * Fetches all direct children of this element that match a particular tag
  123. * name or category (or all children if none is given)
  124. *
  125. * @param $selector tag name or category (optional)
  126. * @return array
  127. */
  128. final public function getChildren(
  129. ?string $selector = null,
  130. ): Vector<XHPChild> {
  131. if ($selector) {
  132. $children = Vector {};
  133. if ($selector[0] == '%') {
  134. $selector = substr($selector, 1);
  135. foreach ($this->children as $child) {
  136. if ($child instanceof :xhp && $child->categoryOf($selector)) {
  137. $children->add($child);
  138. }
  139. }
  140. } else {
  141. $selector = :xhp::element2class($selector);
  142. foreach ($this->children as $child) {
  143. if ($child instanceof $selector) {
  144. $children->add($child);
  145. }
  146. }
  147. }
  148. } else {
  149. $children = new Vector($this->children);
  150. }
  151. return $children;
  152. }
  153. /**
  154. * Fetches the first direct child of the element, or the first child that
  155. * matches the tag if one is given
  156. *
  157. * @param $selector string tag name or category (optional)
  158. * @return element the first child node (with the given selector),
  159. * false if there are no (matching) children
  160. */
  161. final public function getFirstChild(?string $selector = null): ?XHPChild {
  162. if (!$selector) {
  163. return $this->children->get(0);
  164. } else if ($selector[0] == '%') {
  165. $selector = substr($selector, 1);
  166. foreach ($this->children as $child) {
  167. if ($child instanceof :xhp && $child->categoryOf($selector)) {
  168. return $child;
  169. }
  170. }
  171. } else {
  172. $selector = :xhp::element2class($selector);
  173. foreach ($this->children as $child) {
  174. if ($child instanceof $selector) {
  175. return $child;
  176. }
  177. }
  178. }
  179. return null;
  180. }
  181. /**
  182. * Fetches the last direct child of the element, or the last child that
  183. * matches the tag or category if one is given
  184. *
  185. * @param $selector string tag name or category (optional)
  186. * @return element the last child node (with the given selector),
  187. * false if there are no (matching) children
  188. */
  189. final public function getLastChild(?string $selector = null): ?XHPChild {
  190. $temp = $this->getChildren($selector);
  191. if ($temp->count() > 0) {
  192. $count = $temp->count();
  193. return $temp->at($count - 1);
  194. }
  195. return null;
  196. }
  197. /**
  198. * Fetches an attribute from this elements attribute store. If $attr is not
  199. * defined in the store and is not a data- or aria- attribute an exception
  200. * will be thrown. An exception will also be thrown if $attr is required and
  201. * not set.
  202. *
  203. * @param $attr attribute to fetch
  204. * @return value
  205. */
  206. final public function getAttribute(string $attr) {
  207. // Return the attribute if it's there
  208. if ($this->attributes->containsKey($attr)) {
  209. return $this->attributes->get($attr);
  210. }
  211. if (!ReflectionXHPAttribute::IsSpecial($attr)) {
  212. // Get the declaration
  213. $decl = static::__xhpReflectionAttribute($attr);
  214. if ($decl === null) {
  215. throw new XHPAttributeNotSupportedException($this, $attr);
  216. } else if ($decl->isRequired()) {
  217. throw new XHPAttributeRequiredException($this, $attr);
  218. } else {
  219. return $decl->getDefaultValue();
  220. }
  221. } else {
  222. return null;
  223. }
  224. }
  225. final public static function __xhpReflectionAttribute(
  226. string $attr,
  227. ): ?ReflectionXHPAttribute {
  228. $map = static::__xhpReflectionAttributes();
  229. if ($map->containsKey($attr)) {
  230. return $map[$attr];
  231. }
  232. return null;
  233. }
  234. final public static function __xhpReflectionAttributes(
  235. ): Map<string, ReflectionXHPAttribute> {
  236. static $cache = Map { };
  237. $class = static::class;
  238. if (!$cache->containsKey($class)) {
  239. $map = Map { };
  240. $decl = static::__xhpAttributeDeclaration();
  241. foreach ($decl as $name => $attr_decl) {
  242. $map[$name] = new ReflectionXHPAttribute($name, $attr_decl);
  243. }
  244. $cache[$class] = $map;
  245. }
  246. return $cache[$class];
  247. }
  248. final public static function __xhpReflectionChildrenDeclaration(
  249. ): ReflectionXHPChildrenDeclaration {
  250. static $cache = Map { };
  251. $class = static::class;
  252. if (!$cache->containsKey($class)) {
  253. $cache[$class] = new ReflectionXHPChildrenDeclaration(
  254. :xhp::class2element($class),
  255. /* UNSAFE_EXPR: This isn't a static method for some reason - but it
  256. * always returns a static array, and is safe to call statically */
  257. static::__xhpChildrenDeclaration(),
  258. );
  259. }
  260. return $cache[$class];
  261. }
  262. final public static function __xhpReflectionCategoryDeclaration(
  263. ): Set<string> {
  264. return new Set(
  265. /* UNSAFE_EXPR: This isn't a static method for some reason - but it
  266. * always returns a static array, and is safe to call statically */
  267. array_keys(static::__xhpCategoryDeclaration())
  268. );
  269. }
  270. final public function getAttributes(): Map<string, mixed> {
  271. return $this->attributes->toMap();
  272. }
  273. /**
  274. * Sets an attribute in this element's attribute store. If the attribute is
  275. * not defined in the store and is not a data- or aria- attribute an
  276. * exception will be thrown. An exception will also be thrown if the
  277. * attribute value is invalid.
  278. *
  279. * @param $attr attribute to set
  280. * @param $val value
  281. */
  282. final public function setAttribute(string $attr, mixed $value): this {
  283. if (!ReflectionXHPAttribute::IsSpecial($attr)) {
  284. $value = $this->validateAttributeValue($attr, $value);
  285. } else {
  286. $value = (string)$value;
  287. }
  288. $this->attributes->set($attr, $value);
  289. return $this;
  290. }
  291. /**
  292. * Takes an array of key/value pairs and adds each as an attribute.
  293. *
  294. * @param $attrs array of attributes
  295. */
  296. final public function setAttributes(
  297. KeyedTraversable<string, mixed> $attrs,
  298. ): this {
  299. foreach ($attrs as $key => $value) {
  300. $this->setAttribute($key, $value);
  301. }
  302. return $this;
  303. }
  304. /**
  305. * Whether the attribute has been explicitly set to a non-null value by the
  306. * caller (vs. using the default set by "attribute" in the class definition).
  307. *
  308. * @param $attr attribute to check
  309. */
  310. final public function isAttributeSet(string $attr): bool {
  311. return $this->attributes->containsKey($attr);
  312. }
  313. /**
  314. * Removes an attribute from this element's attribute store. An exception
  315. * will be thrown if $attr is not supported.
  316. *
  317. * @param $attr attribute to remove
  318. * @param $val value
  319. */
  320. final public function removeAttribute(string $attr): this {
  321. if (!ReflectionXHPAttribute::IsSpecial($attr)) {
  322. $value = $this->validateAttributeValue($attr, null);
  323. }
  324. $this->attributes->removeKey($attr);
  325. return $this;
  326. }
  327. /**
  328. * Sets an attribute in this element's attribute store. Always foregoes
  329. * validation.
  330. *
  331. * @param $attr attribute to set
  332. * @param $val value
  333. */
  334. final public function forceAttribute(string $attr, mixed $value): this {
  335. $this->attributes->set($attr, $value);
  336. return $this;
  337. }
  338. /**
  339. * Returns all contexts currently set.
  340. *
  341. * @return array All contexts
  342. */
  343. final public function getAllContexts(): Map<string, mixed> {
  344. return $this->context->toMap();
  345. }
  346. /**
  347. * Returns a specific context value. Can include a default if not set.
  348. *
  349. * @param string $key The context key
  350. * @param mixed $default The value to return if not set (optional)
  351. * @return mixed The context value or $default
  352. */
  353. final public function getContext(string $key, mixed $default = null): mixed {
  354. if ($this->context->containsKey($key)) {
  355. return $this->context->get($key);
  356. }
  357. return $default;
  358. }
  359. /**
  360. * Sets a value that will be automatically passed down through a render chain
  361. * and can be referenced by children and composed elements. For instance, if
  362. * a root element sets a context of "admin_mode" = true, then all elements
  363. * that are rendered as children of that root element will receive this
  364. * context WHEN RENDERED. The context will not be available before render.
  365. *
  366. * @param mixed $key Either a key, or an array of key/value pairs
  367. * @param mixed $default if $key is a string, the value to set
  368. * @return :xhp $this
  369. */
  370. final public function setContext(string $key, mixed $value): this {
  371. $this->context->set($key, $value);
  372. return $this;
  373. }
  374. /**
  375. * Sets a value that will be automatically passed down through a render chain
  376. * and can be referenced by children and composed elements. For instance, if
  377. * a root element sets a context of "admin_mode" = true, then all elements
  378. * that are rendered as children of that root element will receive this
  379. * context WHEN RENDERED. The context will not be available before render.
  380. *
  381. * @param Map $context A map of key/value pairs
  382. * @return :xhp $this
  383. */
  384. final public function addContextMap(Map<string, mixed> $context): this {
  385. $this->context->setAll($context);
  386. return $this;
  387. }
  388. /**
  389. * Transfers the context but will not overwrite anything. This is done only
  390. * for rendering because we don't want a parent's context to replace a
  391. * child's context if they have the same key.
  392. *
  393. * @param array $parentContext The context to transfer
  394. */
  395. final protected function __transferContext(
  396. Map<string, mixed> $parentContext,
  397. ): void {
  398. foreach ($parentContext as $key => $value) {
  399. if (!$this->context->containsKey($key)) {
  400. $this->context->set($key, $value);
  401. }
  402. }
  403. }
  404. abstract protected function __flushSubtree(): Awaitable<:x:primitive>;
  405. /**
  406. * Defined in elements by the `attribute` keyword. The declaration is simple.
  407. * There is a keyed array, with each key being an attribute. Each value is
  408. * an array with 4 elements. The first is the attribute type. The second is
  409. * meta-data about the attribute. The third is a default value (null for
  410. * none). And the fourth is whether or not this value is required.
  411. *
  412. * Attribute types are suggested by the TYPE_* constants.
  413. */
  414. protected static function &__xhpAttributeDeclaration(
  415. ): array<string, array<int, mixed>> {
  416. static $decl = array();
  417. return $decl;
  418. }
  419. /**
  420. * Defined in elements by the `category` keyword. This is just a list of all
  421. * categories an element belongs to. Each category is a key with value 1.
  422. */
  423. protected function &__xhpCategoryDeclaration(): array<string, int> {
  424. static $decl = array();
  425. return $decl;
  426. }
  427. /**
  428. * Defined in elements by the `children` keyword. This returns a pattern of
  429. * allowed children. The return value is potentially very complicated. The
  430. * two simplest are 0 and 1 which mean no children and any children,
  431. * respectively. Otherwise you're dealing with an array which is just the
  432. * biggest mess you've ever seen.
  433. */
  434. protected function &__xhpChildrenDeclaration(): mixed {
  435. static $decl = 1;
  436. return $decl;
  437. }
  438. /**
  439. * Throws an exception if $val is not a valid value for the attribute $attr
  440. * on this element.
  441. */
  442. final protected function validateAttributeValue<T>(
  443. string $attr,
  444. T $val,
  445. ): mixed {
  446. $decl = static::__xhpReflectionAttribute($attr);
  447. if ($decl === null) {
  448. throw new XHPAttributeNotSupportedException($this, $attr);
  449. }
  450. if ($val === null) {
  451. return null;
  452. }
  453. switch ($decl->getValueType()) {
  454. case XHPAttributeType::TYPE_STRING:
  455. if (!is_string($val)) {
  456. $val = XHPAttributeCoercion::CoerceToString($this, $attr, $val);
  457. }
  458. break;
  459. case XHPAttributeType::TYPE_BOOL:
  460. if (!is_bool($val)) {
  461. $val = XHPAttributeCoercion::CoerceToBool($this, $attr, $val);
  462. }
  463. break;
  464. case XHPAttributeType::TYPE_INTEGER:
  465. if (!is_int($val)) {
  466. $val = XHPAttributeCoercion::CoerceToInt($this, $attr, $val);
  467. }
  468. break;
  469. case XHPAttributeType::TYPE_FLOAT:
  470. if (!is_float($val)) {
  471. $val = XHPAttributeCoercion::CoerceToFloat($this, $attr, $val);
  472. }
  473. break;
  474. case XHPAttributeType::TYPE_ARRAY:
  475. if (!is_array($val)) {
  476. throw new XHPInvalidAttributeException($this, 'array', $attr, $val);
  477. }
  478. break;
  479. case XHPAttributeType::TYPE_OBJECT:
  480. $class = $decl->getValueClass();
  481. if ($val instanceof $class) {
  482. break;
  483. }
  484. if (enum_exists($class) && $class::isValid($val)) {
  485. break;
  486. }
  487. // Things that are a valid array key without any coercion
  488. if ($class === 'HH\arraykey') {
  489. if (is_int($val) || is_string($val)) {
  490. break;
  491. }
  492. }
  493. if ($class === 'HH\num') {
  494. if (is_int($val) || is_float($val)) {
  495. break;
  496. }
  497. }
  498. throw new XHPInvalidAttributeException(
  499. $this, $class, $attr, $val
  500. );
  501. break;
  502. case XHPAttributeType::TYPE_VAR:
  503. break;
  504. case XHPAttributeType::TYPE_ENUM:
  505. if (!(is_string($val) && $decl->getEnumValues()->contains($val))) {
  506. $enums = 'enum("' . implode('","', $decl->getEnumValues()) . '")';
  507. throw new XHPInvalidAttributeException($this, $enums, $attr, $val);
  508. }
  509. break;
  510. case XHPAttributeType::TYPE_UNSUPPORTED_LEGACY_CALLABLE:
  511. throw new XHPUnsupportedAttributeTypeException(
  512. $this,
  513. 'callable',
  514. $attr,
  515. 'not supported in XHP-Lib 2.0 or higher.',
  516. );
  517. }
  518. return $val;
  519. }
  520. /**
  521. * Validates that this element's children match its children descriptor, and
  522. * throws an exception if that's not the case.
  523. */
  524. final protected function validateChildren(): void {
  525. $decl = self::__xhpReflectionChildrenDeclaration();
  526. $type = $decl->getType();
  527. if ($type === XHPChildrenDeclarationType::ANY_CHILDREN) {
  528. return;
  529. }
  530. if ($type === XHPChildrenDeclarationType::NO_CHILDREN) {
  531. if ($this->children) {
  532. throw new XHPInvalidChildrenException($this, 0);
  533. } else {
  534. return;
  535. }
  536. }
  537. list($ret, $ii) = $this->validateChildrenExpression(
  538. $decl->getExpression(),
  539. 0
  540. );
  541. if (!$ret || $ii < count($this->children)) {
  542. if (isset($this->children[$ii])
  543. && $this->children[$ii] instanceof XHPAlwaysValidChild) {
  544. return;
  545. }
  546. throw new XHPInvalidChildrenException($this, $ii);
  547. }
  548. }
  549. final private function validateChildrenExpression(
  550. ReflectionXHPChildrenExpression $expr,
  551. int $index,
  552. ): (bool, int) {
  553. switch ($expr->getType()) {
  554. case XHPChildrenExpressionType::SINGLE:
  555. // Exactly once -- :fb-thing
  556. return $this->validateChildrenRule($expr, $index);
  557. case XHPChildrenExpressionType::ANY_NUMBER:
  558. // Zero or more times -- :fb-thing*
  559. do {
  560. list($ret, $index) = $this->validateChildrenRule(
  561. $expr,
  562. $index,
  563. );
  564. } while ($ret);
  565. return tuple(true, $index);
  566. case XHPChildrenExpressionType::ZERO_OR_ONE:
  567. // Zero or one times -- :fb-thing?
  568. list($_, $index) = $this->validateChildrenRule(
  569. $expr,
  570. $index,
  571. );
  572. return tuple(true, $index);
  573. case XHPChildrenExpressionType::ONE_OR_MORE:
  574. // One or more times -- :fb-thing+
  575. list($ret, $index) = $this->validateChildrenRule(
  576. $expr,
  577. $index,
  578. );
  579. if (!$ret) {
  580. return tuple(false, $index);
  581. }
  582. do {
  583. list($ret, $index) = $this->validateChildrenRule(
  584. $expr,
  585. $index,
  586. );
  587. } while ($ret);
  588. return tuple(true, $index);
  589. case XHPChildrenExpressionType::SUB_EXPR_SEQUENCE:
  590. // Specific order -- :fb-thing, :fb-other-thing
  591. $oindex = $index;
  592. list($sub_expr_1, $sub_expr_2) = $expr->getSubExpressions();
  593. list($ret, $index) = $this->validateChildrenExpression(
  594. $sub_expr_1,
  595. $index,
  596. );
  597. if ($ret) {
  598. list($ret, $index) = $this->validateChildrenExpression(
  599. $sub_expr_2,
  600. $index,
  601. );
  602. }
  603. if ($ret) {
  604. return tuple(true, $index);
  605. }
  606. return tuple(false, $oindex);
  607. case XHPChildrenExpressionType::SUB_EXPR_DISJUNCTION:
  608. // Either or -- :fb-thing | :fb-other-thing
  609. $oindex = $index;
  610. list($sub_expr_1, $sub_expr_2) = $expr->getSubExpressions();
  611. list($ret, $index) = $this->validateChildrenExpression(
  612. $sub_expr_1,
  613. $index,
  614. );
  615. if (!$ret) {
  616. list($ret, $index) = $this->validateChildrenExpression(
  617. $sub_expr_2,
  618. $index,
  619. );
  620. }
  621. if ($ret) {
  622. return tuple(true, $index);
  623. }
  624. return tuple(false, $oindex);
  625. }
  626. }
  627. final private function validateChildrenRule(
  628. ReflectionXHPChildrenExpression $expr,
  629. int $index,
  630. ): (bool, int) {
  631. switch ($expr->getConstraintType()) {
  632. case XHPChildrenConstraintType::ANY:
  633. if ($this->children->containsKey($index)) {
  634. return tuple(true, $index + 1);
  635. }
  636. return tuple(false, $index);
  637. case XHPChildrenConstraintType::PCDATA:
  638. if ($this->children->containsKey($index) &&
  639. !($this->children->get($index) instanceof :xhp)) {
  640. return tuple(true, $index + 1);
  641. }
  642. return tuple(false, $index);
  643. case XHPChildrenConstraintType::ELEMENT:
  644. $class = $expr->getConstraintString();
  645. if ($this->children->containsKey($index) &&
  646. $this->children->get($index) instanceof $class) {
  647. return tuple(true, $index + 1);
  648. }
  649. return tuple(false, $index);
  650. case XHPChildrenConstraintType::CATEGORY:
  651. if (!$this->children->containsKey($index) ||
  652. !($this->children->get($index) instanceof :xhp)) {
  653. return tuple(false, $index);
  654. }
  655. $category = :xhp::class2element($expr->getConstraintString());
  656. $child = $this->children->get($index);
  657. assert($child instanceof :xhp);
  658. $categories = $child->__xhpCategoryDeclaration();
  659. if (empty($categories[$category])) {
  660. return tuple(false, $index);
  661. }
  662. return tuple(true, $index + 1);
  663. case XHPChildrenConstraintType::SUB_EXPR:
  664. return $this->validateChildrenExpression(
  665. $expr->getSubExpression(),
  666. $index,
  667. );
  668. }
  669. }
  670. /**
  671. * Returns the human-readable `children` declaration as seen in this class's
  672. * source code.
  673. *
  674. * Keeping this wrapper around reflection, as it fits well with
  675. * __getChildrenDescription.
  676. */
  677. public function __getChildrenDeclaration(): string {
  678. return (string) self::__xhpReflectionChildrenDeclaration();
  679. }
  680. /**
  681. * Returns a description of the current children in this element. Maybe
  682. * something like this:
  683. * <div><span>foo</span>bar</div> ->
  684. * :span[%inline],pcdata
  685. */
  686. final public function __getChildrenDescription(): string {
  687. $desc = array();
  688. foreach ($this->children as $child) {
  689. if ($child instanceof :xhp) {
  690. $tmp = ':' . :xhp::class2element(get_class($child));
  691. if ($categories = $child->__xhpCategoryDeclaration()) {
  692. $tmp .= '[%'. implode(',%', array_keys($categories)) . ']';
  693. }
  694. $desc[] = $tmp;
  695. } else {
  696. $desc[] = 'pcdata';
  697. }
  698. }
  699. return implode(',', $desc);
  700. }
  701. final public function categoryOf(string $c): bool {
  702. $categories = $this->__xhpCategoryDeclaration();
  703. if (isset($categories[$c])) {
  704. return true;
  705. }
  706. // XHP parses the category string
  707. $c = str_replace(array(':', '-'), array('__', '_'), $c);
  708. return isset($categories[$c]);
  709. }
  710. }