123 lines
3.7 KiB
PHP
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(
|
|
['"', '"', '"', ''', ''', '''],
|
|
['"', '"', '"', "'", "'", "'"],
|
|
$matches[0]
|
|
);
|
|
},
|
|
$html
|
|
) ?? $html;
|
|
}
|
|
}
|