diff --git a/php-transformer/src/HtmlToBlocks/BlockFactory.php b/php-transformer/src/HtmlToBlocks/BlockFactory.php index ddb06e56..51871f20 100644 --- a/php-transformer/src/HtmlToBlocks/BlockFactory.php +++ b/php-transformer/src/HtmlToBlocks/BlockFactory.php @@ -141,14 +141,7 @@ private function blockHtml(string $name, array $attrs, array $innerBlocks): stri } if ( 'core/icon' === $name ) { - $support = $this->styleSupport($attrs['style'] ?? null); - $iconAttrs = array( - 'class' => $this->mergeClassNames('wp-block-icon', $support['classes'], (string) ($attrs['className'] ?? '')), - 'style' => $support['style'], - 'aria-label' => (string) ($attrs['label'] ?? ''), - 'aria-hidden' => ! empty($attrs['ariaHidden']) ? 'true' : '', - ); - return 'htmlAttrs($iconAttrs) . '>' . ($attrs['svg'] ?? '') . ''; + return ''; } if ( 'core/preformatted' === $name ) { diff --git a/php-transformer/src/HtmlToBlocks/HtmlTransformer.php b/php-transformer/src/HtmlToBlocks/HtmlTransformer.php index 8a694a59..3cdd89ec 100644 --- a/php-transformer/src/HtmlToBlocks/HtmlTransformer.php +++ b/php-transformer/src/HtmlToBlocks/HtmlTransformer.php @@ -4247,14 +4247,51 @@ private function inlineSvgBlockFromElement(DOMElement $element): ?array return null; } - // Keep illustrative/decorative inline SVG inline as a core/html block. - // Externalizing to an `assets/*.svg` file + core/image would be lost in - // WordPress, which blocks SVG uploads by default. The markup is already - // safe-sanitized above (scripts, event handlers, foreignObject, and - // javascript: URLs stripped via sanitizeInlineSvgMarkup + verified by - // isSafeSvgContent), and the original outer SVG preserves - // viewBox/role/aria-label/class. - return $this->createBlock('core/html', array( 'content' => $this->restoreSvgCasing($html) ), array(), $element); + // core/icon is dynamic and references registered icon names; it cannot + // carry arbitrary imported SVG markup. Keep illustrative/decorative SVGs + // inline as sanitized core/html, but make viewBox-only SVGs explicitly + // bounded so they do not render oversized if source CSS is unavailable. + return $this->createBlock('core/html', array( 'content' => $this->restoreSvgCasing($this->ensureInlineSvgSizing($html)) ), array(), $element); + } + + private function ensureInlineSvgSizing(string $html): string + { + if ( 1 !== preg_match('/]*)>/i', $html, $match, PREG_OFFSET_CAPTURE) ) { + return $html; + } + + $attrs = $match[1][0]; + if ( preg_match('/\s(?:width|height)\s*=/i', $attrs) || preg_match('/\sstyle\s*=\s*(["\'])(?:(?!\1).)*(?:width|height)\s*:/i', $attrs) ) { + return $html; + } + + if ( 1 !== preg_match('/\sviewbox\s*=\s*(["\'])([^"\']+)\1/i', $attrs, $viewBoxMatch) ) { + return $html; + } + + $parts = preg_split('/[\s,]+/', trim($viewBoxMatch[2])) ?: array(); + if ( count($parts) < 4 || ! is_numeric($parts[2]) || ! is_numeric($parts[3]) ) { + return $html; + } + + $width = $this->normalizedSvgDimension((float) $parts[2]); + $height = $this->normalizedSvgDimension((float) $parts[3]); + if ( '' === $width || '' === $height ) { + return $html; + } + + $insertAt = $match[0][1] + strlen($match[0][0]) - 1; + return substr($html, 0, $insertAt) . ' width="' . $width . '" height="' . $height . '"' . substr($html, $insertAt); + } + + private function normalizedSvgDimension(float $value): string + { + if ( $value <= 0 ) { + return ''; + } + + $formatted = rtrim(rtrim(sprintf('%.4F', $value), '0'), '.'); + return '' === $formatted ? '' : $formatted; } /** diff --git a/php-transformer/tests/fixtures/parity/html-inline-svg-artwork-preserved.json b/php-transformer/tests/fixtures/parity/html-inline-svg-artwork-preserved.json index 11a6293e..cace47dd 100644 --- a/php-transformer/tests/fixtures/parity/html-inline-svg-artwork-preserved.json +++ b/php-transformer/tests/fixtures/parity/html-inline-svg-artwork-preserved.json @@ -24,7 +24,7 @@ { "path": "blocks", "assert": "count", "count": 1 }, { "path": "fallbacks", "assert": "count", "count": 0 }, { "path": "blocks.0.innerBlocks", "assert": "count", "count": 1 }, - { "path": "blocks.0.innerBlocks.0.attrs.content", "assert": "contains", "value": "" }, + { "path": "blocks.0.innerBlocks.0.attrs.content", "assert": "contains", "value": "" }, { "path": "blocks.0.innerBlocks.0.attrs.content", "assert": "contains", "value": "" }, { "path": "blocks.0.innerBlocks.0.attrs.content", "assert": "contains", "value": "" }, { "path": "blocks.0.innerBlocks.0.attrs.content", "assert": "contains", "value": "" }, diff --git a/php-transformer/tests/fixtures/parity/html-inline-svg-diagram-preserved.json b/php-transformer/tests/fixtures/parity/html-inline-svg-diagram-preserved.json index 17fa1884..7b32e0bf 100644 --- a/php-transformer/tests/fixtures/parity/html-inline-svg-diagram-preserved.json +++ b/php-transformer/tests/fixtures/parity/html-inline-svg-diagram-preserved.json @@ -24,7 +24,7 @@ { "path": "blocks", "assert": "count", "count": 1 }, { "path": "fallbacks", "assert": "count", "count": 0 }, { "path": "blocks.0.innerBlocks", "assert": "count", "count": 1 }, - { "path": "blocks.0.innerBlocks.0.attrs.content", "assert": "contains", "value": "" }, + { "path": "blocks.0.innerBlocks.0.attrs.content", "assert": "contains", "value": "" }, { "path": "blocks.0.innerBlocks.0.attrs.content", "assert": "contains", "value": "" }, { "path": "blocks.0.innerBlocks.0.attrs.content", "assert": "contains", "value": "" }, diff --git a/php-transformer/tests/fixtures/parity/html-inline-svg-sized-html-valid.json b/php-transformer/tests/fixtures/parity/html-inline-svg-sized-html-valid.json new file mode 100644 index 00000000..1b924b9a --- /dev/null +++ b/php-transformer/tests/fixtures/parity/html-inline-svg-sized-html-valid.json @@ -0,0 +1,38 @@ +{ + "schema": "blocks-engine/php-transformer/parity-fixture/v1", + "name": "html-inline-svg-sized-html-valid", + "description": "Preserves arbitrary safe inline SVG as sanitized core/html with explicit intrinsic sizing derived from viewBox, while decorative SVGs inside button-like links are stripped from native core/button labels.", + "source_reference": { + "repo": "php-transformer", + "path": "tests/fixtures/parity/html-inline-svg-sized-html-valid.json", + "notes": "Distilled coverage for viewBox-only decorative SVG, sized hero SVG, and SVG inside a button-like link after core/icon was proven dynamic/registry-bound in WordPress." + }, + "legacy_comparison": { + "skip": true, + "reason": "This upstream primitive fixture has no downstream legacy comparison." + }, + "operation": "html_transformer.transform", + "input": { + "content": "
Reserve" + }, + "expected_blocks": [ + { "path": "blocks.0", "name": "core/group" }, + { "path": "blocks.0.innerBlocks.0", "name": "core/html" }, + { "path": "blocks.0.innerBlocks.1", "name": "core/html" }, + { "path": "blocks.0.innerBlocks.2", "name": "core/buttons" }, + { "path": "blocks.0.innerBlocks.2.innerBlocks.0", "name": "core/button", "attrs": { "className": "icon-cta", "text": "Reserve", "url": "/reserve" } } + ], + "expected_fallbacks": [], + "expect": [ + { "path": "status", "assert": "equals", "value": "success" }, + { "path": "fallbacks", "assert": "count", "count": 0 }, + { "path": "blocks.0.innerBlocks.0.attrs.content", "assert": "contains", "value": "" }, + { "path": "blocks.0.innerBlocks.1.attrs.content", "assert": "contains", "value": "" }, + { "path": "blocks.0.innerBlocks.1.attrs.content", "assert": "contains", "value": "" }, + { "path": "blocks.0.attrs.content", "assert": "contains", "value": "" }, { "path": "blocks.0.attrs.content", "assert": "contains", "value": "" }, { "path": "blocks.0.attrs.content", "assert": "not_contains", "value": "" }, + { "path": "blocks.0.innerBlocks.0.innerBlocks.0.attrs.content", "assert": "contains", "value": "" }, { "path": "blocks.0.innerBlocks.0.innerBlocks.0.attrs.content", "assert": "contains", "value": "" }, { "path": "blocks.0.innerBlocks.0.innerBlocks.0.attrs.content", "assert": "not_contains", "value": "data:image/svg+xml" }, { "path": "assets", "assert": "count", "count": 0 },