tagName); // Заголовки → text if (in_array($tag, ['h1','h2','h3','h4','h5','h6'])) { $text = trim($el->textContent); if ($text !== '') { $raw[] = $this->mkField('heading', 'Заголовок', 'text', $el, null, $var); } return; } // Абзац → textarea if ($tag === 'p') { $text = trim($el->textContent); if ($text !== '') { $raw[] = $this->mkField('text', 'Текст', 'textarea', $el, null, $var); } return; } // Картинка → image if ($tag === 'img') { if ($el->getAttribute('src') !== '') { $raw[] = $this->mkField('image', 'Изображение', 'image', $el, 'src', $var); } return; } // Ссылка → url + text if ($tag === 'a') { $href = $el->getAttribute('href'); $text = trim($el->textContent); if ($text !== '') { $raw[] = $this->mkField('link_text', 'Текст ссылки', 'text', $el, null, $var); } if ($href !== '' && $href !== '#') { $raw[] = $this->mkField('link_url', 'URL ссылки', 'url', $el, 'href', $var); } return; } // Листовой элемент с коротким текстом → text $childEls = $this->elementChildren($el); if (empty($childEls)) { $text = trim($el->textContent); if ($text !== '' && mb_strlen($text) <= 300) { $raw[] = $this->mkField('label', 'Подпись', 'text', $el, null, $var); } return; } // Иначе рекурсируем foreach ($childEls as $child) { $this->extractFields($child, $raw, $var); } } // ── Создание поля ───────────────────────────────────────────────────── private function mkField(string $base, string $label, string $type, DOMElement $el, ?string $attr, string $var): array { $name = $base; $i = 2; while (isset($this->usedNames[$name])) { $name = $base . '_' . $i++; } $this->usedNames[$name] = true; return [ 'name' => $name, 'label' => $label, 'type' => $type, '_el' => $el, '_attr' => $attr, '_var' => $var, ]; } // ── Токенизация DOM ─────────────────────────────────────────────────── /** Заменяет содержимое/атрибуты в DOM на уникальные токены. */ private function applyTokens(array $raw): void { foreach ($raw as $field) { $token = 'BLTKN' . strtoupper($field['name']) . 'END'; $expr = "{{ {$field['_var']}['{$field['name']}'] }}"; $this->tokens[$token] = $expr; /** @var DOMElement $el */ $el = $field['_el']; if ($field['_attr'] !== null) { $el->setAttribute($field['_attr'], $token); } else { while ($el->firstChild) { $el->removeChild($el->firstChild); } $el->appendChild($this->dom->createTextNode($token)); } } } private function replaceTokens(string $html): string { foreach ($this->tokens as $token => $expr) { $html = str_replace($token, $expr, $html); } return $html; } // ── Сериализация ────────────────────────────────────────────────────── private function serializeChildren(DOMElement $el): string { $html = ''; foreach ($el->childNodes as $child) { $html .= $this->dom->saveHTML($child); } return $html; } // ── Хелперы ─────────────────────────────────────────────────────────── /** Возвращает только публичные поля (без служебных _el, _attr, _var). */ private function publicFields(array $raw): array { return array_values(array_map(fn($f) => array_filter( $f, fn($k) => !str_starts_with($k, '_'), ARRAY_FILTER_USE_KEY ), $raw)); } /** Возвращает дочерние DOM-элементы (без текстовых узлов). */ private function elementChildren(DOMElement $el): array { $result = []; foreach ($el->childNodes as $node) { if ($node->nodeType === XML_ELEMENT_NODE) { $result[] = $node; } } return $result; } }