Files
2026-09-15 10:35:56 +08:00

123 lines
3.7 KiB
PHP

<?php
declare(strict_types=1);
namespace Esun\Translation\Model\Html;
use DOMDocument;
use DOMNode;
use DOMText;
use DOMXPath;
class HtmlTextExtractor
{
private const ROOT_ID = 'esun-translation-root';
private const DIRECTIVE_TOKEN_PREFIX = '__ESUN_HTML_DIRECTIVE_';
public function extract(string $html): HtmlFragment
{
[$protectedHtml, $directivePlaceholders] = $this->protectMagentoDirectives($html);
$document = $this->createDocument($protectedHtml);
$xpath = new DOMXPath($document);
$query = sprintf(
'//*[@id="%s"]//text()[normalize-space(.) != "" and not(ancestor::script) and not(ancestor::style) and not(ancestor::code) and not(ancestor::pre)]',
self::ROOT_ID
);
$nodes = $xpath->query($query);
$textNodes = [];
$texts = [];
$index = 0;
if ($nodes !== false) {
foreach ($nodes as $node) {
if (!$node instanceof DOMText) {
continue;
}
$text = $this->restoreMagentoDirectives($node->nodeValue ?? '', $directivePlaceholders);
if (trim($text) === '') {
continue;
}
$id = 'html_' . $index++;
$textNodes[$id] = $node;
$texts[$id] = $text;
}
}
$root = $document->getElementById(self::ROOT_ID);
if (!$root) {
throw new \RuntimeException('Unable to parse HTML fragment.');
}
return new HtmlFragment($document, $root, $textNodes, $texts, $directivePlaceholders);
}
private function createDocument(string $html): DOMDocument
{
$document = new DOMDocument('1.0', 'UTF-8');
$wrappedHtml = sprintf(
'<?xml encoding="UTF-8"><div id="%s">%s</div>',
self::ROOT_ID,
$html
);
$previous = libxml_use_internal_errors(true);
$loaded = $document->loadHTML($wrappedHtml, LIBXML_HTML_NODEFDTD | LIBXML_HTML_NOIMPLIED);
libxml_clear_errors();
libxml_use_internal_errors($previous);
if (!$loaded) {
throw new \RuntimeException('Unable to parse HTML fragment.');
}
return $document;
}
/**
* @return array{0: string, 1: array<string, string>}
*/
private function protectMagentoDirectives(string $html): array
{
$placeholders = [];
$index = 0;
$protected = preg_replace_callback(
'/\{\{.*?\}\}/s',
function (array $matches) use (&$placeholders, &$index): string {
$token = self::DIRECTIVE_TOKEN_PREFIX . $index++ . '__';
$placeholders[$token] = $this->normalizeMagentoDirectiveQuotes($matches[0]);
return $token;
},
$html
);
return [$protected ?? $html, $placeholders];
}
/**
* @param array<string, string> $placeholders
*/
private function restoreMagentoDirectives(string $html, array $placeholders): string
{
$restored = $placeholders === [] ? $html : strtr($html, $placeholders);
return $this->normalizeMagentoDirectiveQuotes($restored);
}
private function normalizeMagentoDirectiveQuotes(string $html): string
{
return preg_replace_callback(
'/\{\{.*?\}\}/s',
static function (array $matches): string {
return str_ireplace(
['&quot;', '&#34;', '&#x22;', '&apos;', '&#39;', '&#x27;'],
['"', '"', '"', "'", "'", "'"],
$matches[0]
);
},
$html
) ?? $html;
}
}