BlockLayoutHelpersTrait.php 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. <?php
  2. namespace App\Support\Traits;
  3. use DOMElement;
  4. /*
  5. * Трейт: низкоуровневые хелперы BlockLayoutParser —
  6. * извлечение полей, токенизация DOM, сериализация.
  7. * Используется классом BlockLayoutParser.
  8. */
  9. trait BlockLayoutHelpersTrait
  10. {
  11. // ── Извлечение полей из DOM-элемента ──────────────────────────────────
  12. /** Рекурсивно собирает поля из $el в массив $raw. */
  13. private function extractFields(DOMElement $el, array &$raw, string $var): void
  14. {
  15. $tag = strtolower($el->tagName);
  16. // Заголовки → text
  17. if (in_array($tag, ['h1','h2','h3','h4','h5','h6'])) {
  18. $text = trim($el->textContent);
  19. if ($text !== '') {
  20. $raw[] = $this->mkField('heading', 'Заголовок', 'text', $el, null, $var);
  21. }
  22. return;
  23. }
  24. // Абзац → textarea
  25. if ($tag === 'p') {
  26. $text = trim($el->textContent);
  27. if ($text !== '') {
  28. $raw[] = $this->mkField('text', 'Текст', 'textarea', $el, null, $var);
  29. }
  30. return;
  31. }
  32. // Картинка → image
  33. if ($tag === 'img') {
  34. if ($el->getAttribute('src') !== '') {
  35. $raw[] = $this->mkField('image', 'Изображение', 'image', $el, 'src', $var);
  36. }
  37. return;
  38. }
  39. // Ссылка → url + text
  40. if ($tag === 'a') {
  41. $href = $el->getAttribute('href');
  42. $text = trim($el->textContent);
  43. if ($text !== '') {
  44. $raw[] = $this->mkField('link_text', 'Текст ссылки', 'text', $el, null, $var);
  45. }
  46. if ($href !== '' && $href !== '#') {
  47. $raw[] = $this->mkField('link_url', 'URL ссылки', 'url', $el, 'href', $var);
  48. }
  49. return;
  50. }
  51. // Листовой элемент с коротким текстом → text
  52. $childEls = $this->elementChildren($el);
  53. if (empty($childEls)) {
  54. $text = trim($el->textContent);
  55. if ($text !== '' && mb_strlen($text) <= 300) {
  56. $raw[] = $this->mkField('label', 'Подпись', 'text', $el, null, $var);
  57. }
  58. return;
  59. }
  60. // Иначе рекурсируем
  61. foreach ($childEls as $child) {
  62. $this->extractFields($child, $raw, $var);
  63. }
  64. }
  65. // ── Создание поля ─────────────────────────────────────────────────────
  66. private function mkField(string $base, string $label, string $type, DOMElement $el, ?string $attr, string $var): array
  67. {
  68. $name = $base;
  69. $i = 2;
  70. while (isset($this->usedNames[$name])) {
  71. $name = $base . '_' . $i++;
  72. }
  73. $this->usedNames[$name] = true;
  74. return [
  75. 'name' => $name,
  76. 'label' => $label,
  77. 'type' => $type,
  78. '_el' => $el,
  79. '_attr' => $attr,
  80. '_var' => $var,
  81. ];
  82. }
  83. // ── Токенизация DOM ───────────────────────────────────────────────────
  84. /** Заменяет содержимое/атрибуты в DOM на уникальные токены. */
  85. private function applyTokens(array $raw): void
  86. {
  87. foreach ($raw as $field) {
  88. $token = 'BLTKN' . strtoupper($field['name']) . 'END';
  89. $expr = "{{ {$field['_var']}['{$field['name']}'] }}";
  90. $this->tokens[$token] = $expr;
  91. /** @var DOMElement $el */
  92. $el = $field['_el'];
  93. if ($field['_attr'] !== null) {
  94. $el->setAttribute($field['_attr'], $token);
  95. } else {
  96. while ($el->firstChild) {
  97. $el->removeChild($el->firstChild);
  98. }
  99. $el->appendChild($this->dom->createTextNode($token));
  100. }
  101. }
  102. }
  103. private function replaceTokens(string $html): string
  104. {
  105. foreach ($this->tokens as $token => $expr) {
  106. $html = str_replace($token, $expr, $html);
  107. }
  108. return $html;
  109. }
  110. // ── Сериализация ──────────────────────────────────────────────────────
  111. private function serializeChildren(DOMElement $el): string
  112. {
  113. $html = '';
  114. foreach ($el->childNodes as $child) {
  115. $html .= $this->dom->saveHTML($child);
  116. }
  117. return $html;
  118. }
  119. // ── Хелперы ───────────────────────────────────────────────────────────
  120. /** Возвращает только публичные поля (без служебных _el, _attr, _var). */
  121. private function publicFields(array $raw): array
  122. {
  123. return array_values(array_map(fn($f) => array_filter(
  124. $f,
  125. fn($k) => !str_starts_with($k, '_'),
  126. ARRAY_FILTER_USE_KEY
  127. ), $raw));
  128. }
  129. /** Возвращает дочерние DOM-элементы (без текстовых узлов). */
  130. private function elementChildren(DOMElement $el): array
  131. {
  132. $result = [];
  133. foreach ($el->childNodes as $node) {
  134. if ($node->nodeType === XML_ELEMENT_NODE) {
  135. $result[] = $node;
  136. }
  137. }
  138. return $result;
  139. }
  140. }