HtmlRenderer.php 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. <?php
  2. declare(strict_types=1);
  3. namespace Termwind;
  4. use DOMDocument;
  5. use DOMNode;
  6. use Termwind\Html\CodeRenderer;
  7. use Termwind\Html\PreRenderer;
  8. use Termwind\Html\TableRenderer;
  9. use Termwind\ValueObjects\Node;
  10. /**
  11. * @internal
  12. */
  13. final class HtmlRenderer
  14. {
  15. /**
  16. * Renders the given html.
  17. */
  18. public function render(string $html, int $options): void
  19. {
  20. $this->parse($html)->render($options);
  21. }
  22. /**
  23. * Parses the given html.
  24. */
  25. public function parse(string $html): Components\Element
  26. {
  27. $dom = new DOMDocument;
  28. if (strip_tags($html) === $html) {
  29. return Termwind::span($html);
  30. }
  31. $html = '<?xml encoding="UTF-8"><!DOCTYPE html><html><body>'.trim($html).'</body></html>';
  32. $dom->loadHTML($html, LIBXML_NOERROR | LIBXML_COMPACT | LIBXML_HTML_NODEFDTD | LIBXML_NOBLANKS | LIBXML_NOXMLDECL);
  33. /** @var DOMNode $body */
  34. $body = $dom->getElementsByTagName('body')->item(0);
  35. $el = $this->convert(new Node($body));
  36. // @codeCoverageIgnoreStart
  37. return is_string($el)
  38. ? Termwind::span($el)
  39. : $el;
  40. // @codeCoverageIgnoreEnd
  41. }
  42. /**
  43. * Convert a tree of DOM nodes to a tree of termwind elements.
  44. */
  45. private function convert(Node $node): Components\Element|string
  46. {
  47. $children = [];
  48. if ($node->isName('table')) {
  49. return (new TableRenderer)->toElement($node);
  50. } elseif ($node->isName('code')) {
  51. return (new CodeRenderer)->toElement($node);
  52. } elseif ($node->isName('pre')) {
  53. return (new PreRenderer)->toElement($node);
  54. }
  55. foreach ($node->getChildNodes() as $child) {
  56. $children[] = $this->convert($child);
  57. }
  58. $children = array_filter($children, fn ($child) => $child !== '');
  59. return $this->toElement($node, $children);
  60. }
  61. /**
  62. * Convert a given DOM node to it's termwind element equivalent.
  63. *
  64. * @param array<int, Components\Element|string> $children
  65. */
  66. private function toElement(Node $node, array $children): Components\Element|string
  67. {
  68. if ($node->isText() || $node->isComment()) {
  69. return (string) $node;
  70. }
  71. /** @var array<string, mixed> $properties */
  72. $properties = [
  73. 'isFirstChild' => $node->isFirstChild(),
  74. ];
  75. $styles = $node->getClassAttribute();
  76. return match ($node->getName()) {
  77. 'body' => $children[0], // Pick only the first element from the body node
  78. 'div' => Termwind::div($children, $styles, $properties),
  79. 'p' => Termwind::paragraph($children, $styles, $properties),
  80. 'ul' => Termwind::ul($children, $styles, $properties),
  81. 'ol' => Termwind::ol($children, $styles, $properties),
  82. 'li' => Termwind::li($children, $styles, $properties),
  83. 'dl' => Termwind::dl($children, $styles, $properties),
  84. 'dt' => Termwind::dt($children, $styles, $properties),
  85. 'dd' => Termwind::dd($children, $styles, $properties),
  86. 'span' => Termwind::span($children, $styles, $properties),
  87. 'br' => Termwind::breakLine($styles, $properties),
  88. 'strong' => Termwind::span($children, $styles, $properties)->strong(),
  89. 'b' => Termwind::span($children, $styles, $properties)->fontBold(),
  90. 'em', 'i' => Termwind::span($children, $styles, $properties)->italic(),
  91. 'u' => Termwind::span($children, $styles, $properties)->underline(),
  92. 's' => Termwind::span($children, $styles, $properties)->lineThrough(),
  93. 'a' => Termwind::anchor($children, $styles, $properties)->href($node->getAttribute('href')),
  94. 'hr' => Termwind::hr($styles, $properties),
  95. default => Termwind::div($children, $styles, $properties),
  96. };
  97. }
  98. }