PageRenderTime 44ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/src/FluentDOM/DOM/Document.php

http://github.com/ThomasWeinert/FluentDOM
PHP | 364 lines | 186 code | 25 blank | 153 comment | 28 complexity | 87b8592bb8976c8007e52c474b05e897 MD5 | raw file
  1. <?php
  2. /*
  3. * FluentDOM
  4. *
  5. * @link https://thomas.weinert.info/FluentDOM/
  6. * @copyright Copyright 2009-2021 FluentDOM Contributors
  7. * @license http://www.opensource.org/licenses/mit-license.php The MIT License
  8. *
  9. */
  10. declare(strict_types=1);
  11. namespace FluentDOM\DOM {
  12. use FluentDOM\Utility\Namespaces;
  13. use FluentDOM\Utility\QualifiedName;
  14. /**
  15. * @method Attribute createAttributeNS($namespaceURI, $name)
  16. * @method CdataSection createCdataSection($data)
  17. * @method Comment createComment($data)
  18. * @method DocumentFragment createDocumentFragment()
  19. * @method ProcessingInstruction createProcessingInstruction($target, $data = NULL)
  20. * @method Text createTextNode($content)
  21. *
  22. * @property-read Element $documentElement
  23. * @property-read Element $firstElementChild
  24. * @property-read Element $lastElementChild
  25. */
  26. class Document extends \DOMDocument implements Node\ParentNode {
  27. use
  28. Node\ParentNode\Properties,
  29. Node\QuerySelector\Implementation,
  30. Node\Xpath;
  31. /**
  32. * @var Xpath
  33. */
  34. private $_xpath;
  35. /**
  36. * @var Namespaces
  37. */
  38. private $_namespaces;
  39. /**
  40. * Map dom node classes to extended descendants.
  41. *
  42. * @var array
  43. */
  44. private static $_classes = [
  45. 'DOMDocument' => self::class,
  46. 'DOMAttr' => Attribute::class,
  47. 'DOMCdataSection' => CdataSection::class,
  48. 'DOMComment' => Comment::class,
  49. 'DOMElement' => Element::class,
  50. 'DOMProcessingInstruction' => ProcessingInstruction::class,
  51. 'DOMText' => Text::class,
  52. 'DOMDocumentFragment' => DocumentFragment::class,
  53. 'DOMEntityReference' => EntityReference::class
  54. ];
  55. /**
  56. * @param string $version
  57. * @param string $encoding
  58. */
  59. public function __construct($version = '1.0', $encoding = 'UTF-8') {
  60. parent::__construct($version, $encoding ?: 'UTF-8');
  61. foreach (self::$_classes as $superClass => $className) {
  62. $this->registerNodeClass($superClass, $className);
  63. }
  64. $this->_namespaces = new Namespaces();
  65. }
  66. public function __clone() {
  67. $this->_namespaces = clone $this->_namespaces;
  68. }
  69. /**
  70. * Generate an xpath instance for the document, if the document of the
  71. * xpath instance does not match the document, regenerate it.
  72. *
  73. * @return Xpath
  74. */
  75. public function xpath(): Xpath {
  76. if (
  77. $this->_xpath instanceof Xpath &&
  78. $this->_xpath->document === $this
  79. ) {
  80. return $this->_xpath;
  81. }
  82. $this->_xpath = new Xpath($this);
  83. foreach ($this->_namespaces as $prefix => $namespaceURI) {
  84. $this->_xpath->registerNamespace($prefix, $namespaceURI);
  85. }
  86. return $this->_xpath;
  87. }
  88. /**
  89. * register a namespace prefix for the document, it will be used in
  90. * createElement and setAttribute
  91. *
  92. * @param string $prefix
  93. * @param string $namespaceURI
  94. * @throws \LogicException
  95. */
  96. public function registerNamespace(string $prefix, string $namespaceURI): void {
  97. $this->_namespaces[$prefix] = $namespaceURI;
  98. if (NULL !== $this->_xpath && $prefix !== '#default') {
  99. $this->_xpath->registerNamespace($prefix, $namespaceURI);
  100. }
  101. }
  102. /**
  103. * Get set the namespaces registered for the document object.
  104. *
  105. * If the argument is provided ALL namespaces will be replaced.
  106. *
  107. * @param array|\Traversable $namespaces
  108. * @return Namespaces
  109. * @throws \LogicException
  110. */
  111. public function namespaces($namespaces = NULL): Namespaces {
  112. if (NULL !== $namespaces) {
  113. $this->_namespaces->assign([]);
  114. foreach($namespaces as $prefix => $namespaceURI) {
  115. $this->registerNamespace($prefix, $namespaceURI);
  116. }
  117. }
  118. return $this->_namespaces;
  119. }
  120. /**
  121. * If here is a ':' in the element name, consider it a namespace prefix
  122. * registered on the document.
  123. *
  124. * Allow to add a text content and attributes directly.
  125. *
  126. * If $content is an array, the $content argument will be merged with the $attributes
  127. * argument.
  128. *
  129. * @param string $qualifiedName
  130. * @param string|array $content
  131. * @param array|NULL $attributes
  132. * @return Element
  133. *@throws \LogicException
  134. */
  135. public function createElement($qualifiedName, $content = NULL, array $attributes = NULL): Element {
  136. [$prefix, $localName] = QualifiedName::split($qualifiedName);
  137. $namespaceURI = '';
  138. if ($prefix !== FALSE) {
  139. if (empty($prefix)) {
  140. $qualifiedName = $localName;
  141. } else {
  142. if ($this->namespaces()->isReservedPrefix($prefix)) {
  143. throw new \LogicException(
  144. \sprintf('Can not use reserved namespace prefix "%s" in element name.', $prefix)
  145. );
  146. }
  147. $namespaceURI = (string)$this->namespaces()->resolveNamespace($prefix);
  148. }
  149. } else {
  150. $namespaceURI = (string)$this->namespaces()->resolveNamespace('#default');
  151. }
  152. if ($namespaceURI !== '') {
  153. $node = $this->createElementNS($namespaceURI, $qualifiedName);
  154. } elseif (isset($this->_namespaces['#default'])) {
  155. $node = $this->createElementNS('', $qualifiedName);
  156. } else {
  157. $node = parent::createElement($qualifiedName);
  158. }
  159. $this->appendAttributes($node, $content, $attributes);
  160. $this->appendContent($node, $content);
  161. return $node;
  162. }
  163. /**
  164. * @param string $namespaceURI
  165. * @param string $qualifiedName
  166. * @param string|NULL $content
  167. * @return Element
  168. */
  169. public function createElementNS($namespaceURI, $qualifiedName, $content = NULL): Element {
  170. /** @var Element $node */
  171. $node = parent::createElementNS($namespaceURI, $qualifiedName);
  172. $this->appendContent($node, $content);
  173. return $node;
  174. }
  175. /**
  176. * If here is a ':' in the attribute name, consider it a namespace prefix
  177. * registered on the document.
  178. *
  179. * Allow to add a attribute value directly.
  180. *
  181. * @param string $name
  182. * @param string|NULL $value
  183. * @return Attribute
  184. * @throws \LogicException
  185. */
  186. public function createAttribute($name, $value = NULL): Attribute {
  187. [$prefix] = QualifiedName::split($name);
  188. if (empty($prefix)) {
  189. $node = parent::createAttribute($name);
  190. } else {
  191. $node = $this->createAttributeNS($this->namespaces()->resolveNamespace($prefix), $name);
  192. }
  193. if (NULL !== $value) {
  194. $node->value = $value;
  195. }
  196. return $node;
  197. }
  198. /**
  199. * Overload appendElement to add a text content and attributes directly.
  200. *
  201. * @param string $name
  202. * @param string $content
  203. * @param array|NULL $attributes
  204. * @return Element
  205. * @throws \LogicException
  206. */
  207. public function appendElement(string $name, $content = '', array $attributes = NULL): Element {
  208. $this->appendChild(
  209. $node = $this->createElement($name, $content, $attributes)
  210. );
  211. return $node;
  212. }
  213. /**
  214. * @param \DOMElement $node
  215. * @param string|array|NULL $content
  216. * @param array|NULL $attributes
  217. */
  218. private function appendAttributes(\DOMElement $node, $content = NULL, array $attributes = NULL): void {
  219. if (\is_array($content)) {
  220. /** @noinspection CallableParameterUseCaseInTypeContextInspection */
  221. $attributes = (NULL === $attributes) ? $content : \array_merge($content, $attributes);
  222. }
  223. if (!empty($attributes)) {
  224. foreach ($attributes as $attributeName => $attributeValue) {
  225. $node->setAttribute($attributeName, $attributeValue);
  226. }
  227. }
  228. }
  229. /**
  230. * @param \DOMElement $node
  231. * @param string|array|NULL $content
  232. */
  233. private function appendContent(\DOMElement $node, $content = NULL): void {
  234. if (!((empty($content) && !\is_numeric($content)) || \is_array($content) )) {
  235. $node->appendChild($this->createTextNode((string)$content));
  236. }
  237. }
  238. /**
  239. * Allow to save XML fragments, providing a node list
  240. *
  241. * Overloading saveXML() with a removed type hint triggers an E_STRICT error,
  242. * so we the function needs a new name. :-(
  243. *
  244. * @param \DOMNode|\DOMNodeList|NULL $context
  245. * @param int $options
  246. * @return string
  247. */
  248. public function toXml($context = NULL, int $options = 0): string {
  249. if ($context instanceof \DOMNodeList) {
  250. $result = '';
  251. foreach ($context as $node) {
  252. $result .= $this->saveXML($node, $options);
  253. }
  254. return $result;
  255. }
  256. return $this->saveXML($context, $options);
  257. }
  258. /**
  259. * Allow to cast the document to string, returning the whole XML.
  260. *
  261. * @return string
  262. */
  263. public function __toString(): string {
  264. return $this->saveXML();
  265. }
  266. /**
  267. * Allow to save HTML fragments, providing a node list.
  268. *
  269. * This is an alias for the extended saveHTML() method. Make it
  270. * consistent with toXml()
  271. *
  272. * @param \DOMNode|\DOMNodeList|NULL $context
  273. * @return string
  274. */
  275. public function toHtml($context = NULL): string {
  276. return $this->saveHTML($context);
  277. }
  278. /**
  279. * Allow to save HTML fragments, providing a node list
  280. *
  281. * @param \DOMNode|\DOMNodeList|NULL $context
  282. * @return string
  283. */
  284. public function saveHTML($context = NULL): string {
  285. if ($context instanceof \DOMDocumentFragment) {
  286. $context = $context->childNodes;
  287. }
  288. if ($context instanceof \DOMNodeList) {
  289. $result = '';
  290. foreach ($context as $node) {
  291. $result .= parent::saveHTML($node);
  292. }
  293. return $result;
  294. }
  295. if (NULL === $context) {
  296. $result = '';
  297. $textOnly = TRUE;
  298. $elementCount = 0;
  299. foreach ($this->childNodes as $node) {
  300. $textOnly = $textOnly && $node instanceof \DOMText;
  301. $elementCount += $node instanceof \DOMElement ? 1 : 0;
  302. if ($node instanceof \DOMDocumentType) {
  303. $result .= parent::saveXML($node)."\n";
  304. } else {
  305. $result .= parent::saveHTML($node);
  306. }
  307. }
  308. return $textOnly || $elementCount > 1 ? $result : $result."\n";
  309. }
  310. return parent::saveHTML($context);
  311. }
  312. /**
  313. * Allow getElementsByTagName to use the defined namespaces.
  314. *
  315. * @param string $qualifiedName
  316. * @return \DOMNodeList
  317. * @throws \LogicException
  318. */
  319. public function getElementsByTagName($qualifiedName): \DOMNodeList {
  320. list($prefix, $localName) = QualifiedName::split($qualifiedName);
  321. $namespaceURI = (string)$this->namespaces()->resolveNamespace((string)$prefix);
  322. if ($namespaceURI !== '') {
  323. return $this->getElementsByTagNameNS($namespaceURI, $localName);
  324. }
  325. return parent::getElementsByTagName($localName);
  326. }
  327. /**
  328. * @param string|null $qualifiedName
  329. * @param string|null $publicId
  330. * @param string|null $systemId
  331. * @return \DOMDocumentType
  332. */
  333. public function createDocumentType(
  334. string $qualifiedName = NULL, string $publicId = NULL, string $systemId = NULL
  335. ): \DOMDocumentType {
  336. return (new Implementation())->createDocumentType($qualifiedName, (string)$publicId, (string)$systemId);
  337. }
  338. }
  339. }