Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 1 addition & 8 deletions php-transformer/src/HtmlToBlocks/BlockFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 '<div' . $this->htmlAttrs($iconAttrs) . '>' . ($attrs['svg'] ?? '') . '</div>';
return '';
}

if ( 'core/preformatted' === $name ) {
Expand Down
53 changes: 45 additions & 8 deletions php-transformer/src/HtmlToBlocks/HtmlTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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('/<svg\b([^>]*)>/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;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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": "<svg viewBox=\"0 0 600 600\" role=\"img\" aria-label=\"Mara Vale cover\">" },
{ "path": "blocks.0.innerBlocks.0.attrs.content", "assert": "contains", "value": "<svg viewBox=\"0 0 600 600\" role=\"img\" aria-label=\"Mara Vale cover\" width=\"600\" height=\"600\">" },
{ "path": "blocks.0.innerBlocks.0.attrs.content", "assert": "contains", "value": "<!-- Background -->" },
{ "path": "blocks.0.innerBlocks.0.attrs.content", "assert": "contains", "value": "<rect width=\"600\" height=\"600\" fill=\"#1a1a2e\"></rect>" },
{ "path": "blocks.0.innerBlocks.0.attrs.content", "assert": "contains", "value": "<circle cx=\"300\" cy=\"240\" r=\"160\" fill=\"#e94560\"></circle>" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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": "<svg viewBox=\"0 0 400 300\" aria-hidden=\"true\">" },
{ "path": "blocks.0.innerBlocks.0.attrs.content", "assert": "contains", "value": "<svg viewBox=\"0 0 400 300\" aria-hidden=\"true\" width=\"400\" height=\"300\">" },
{ "path": "blocks.0.innerBlocks.0.attrs.content", "assert": "contains", "value": "<linearGradient id=\"pipe-grad\"" },
{ "path": "blocks.0.innerBlocks.0.attrs.content", "assert": "contains", "value": "<stop offset=\"0%\" stop-color=\"#88c\"></stop>" },
{ "path": "blocks.0.innerBlocks.0.attrs.content", "assert": "contains", "value": "<line x1=\"200\" y1=\"0\" x2=\"200\" y2=\"300\" stroke=\"#333\" stroke-width=\"6\"></line>" },
Expand Down
Original file line number Diff line number Diff line change
@@ -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": "<main><svg class=\"status-icon\" viewBox=\"0 0 24 24\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"></circle></svg><svg class=\"hero-flame-svg\" viewBox=\"0 0 100 160\" role=\"img\" aria-label=\"Hero flame\"><defs><linearGradient id=\"flame-grad\" x1=\"50\" y1=\"0\" x2=\"50\" y2=\"160\" gradientUnits=\"userSpaceOnUse\"><stop offset=\"0%\" stop-color=\"#ff9060\"></stop><stop offset=\"100%\" stop-color=\"#e06012\"></stop></linearGradient></defs><path d=\"M50 0 C90 56 82 122 50 160 C18 122 10 56 50 0Z\" fill=\"url(#flame-grad)\"></path></svg><a class=\"icon-cta\" href=\"/reserve\"><svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" aria-hidden=\"true\"><path d=\"M2 8h12\"></path></svg><span>Reserve</span></a></main>"
},
"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": "<svg class=\"status-icon\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" width=\"24\" height=\"24\">" },
{ "path": "blocks.0.innerBlocks.1.attrs.content", "assert": "contains", "value": "<svg class=\"hero-flame-svg\" viewBox=\"0 0 100 160\" role=\"img\" aria-label=\"Hero flame\" width=\"100\" height=\"160\">" },
{ "path": "blocks.0.innerBlocks.1.attrs.content", "assert": "contains", "value": "<linearGradient id=\"flame-grad\"" },
{ "path": "serialized_blocks", "assert": "contains", "value": "<!-- wp:html" },
{ "path": "serialized_blocks", "assert": "contains", "value": "<!-- wp:button" },
{ "path": "serialized_blocks", "assert": "not_contains", "value": "<!-- wp:icon" },
{ "path": "assets", "assert": "count", "count": 0 },
{ "path": "coverage.0.fallback_count", "assert": "equals", "value": 0 }
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
{ "path": "status", "assert": "equals", "value": "success" },
{ "path": "blocks", "assert": "count", "count": 2 },
{ "path": "fallbacks", "assert": "count", "count": 0 },
{ "path": "blocks.0.attrs.content", "assert": "contains", "value": "<svg viewBox=\"0 0 20 20\">" },
{ "path": "blocks.0.attrs.content", "assert": "contains", "value": "<svg viewBox=\"0 0 20 20\" width=\"20\" height=\"20\">" },
{ "path": "blocks.0.attrs.content", "assert": "contains", "value": "<circle cx=\"10\" cy=\"10\" r=\"8\"></circle>" },
{ "path": "blocks.0.attrs.content", "assert": "not_contains", "value": "<script" },
{ "path": "blocks.0.attrs.content", "assert": "not_contains", "value": "onclick" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"expect": [
{ "path": "status", "assert": "equals", "value": "success" },
{ "path": "blocks.0.innerBlocks", "assert": "count", "count": 2 },
{ "path": "blocks.0.innerBlocks.0.innerBlocks.0.attrs.content", "assert": "contains", "value": "<svg class=\"product-image\" role=\"img\" aria-label=\"Ceramic bowl\" viewBox=\"0 0 10 10\">" },
{ "path": "blocks.0.innerBlocks.0.innerBlocks.0.attrs.content", "assert": "contains", "value": "<svg class=\"product-image\" role=\"img\" aria-label=\"Ceramic bowl\" viewBox=\"0 0 10 10\" width=\"10\" height=\"10\">" },
{ "path": "blocks.0.innerBlocks.0.innerBlocks.0.attrs.content", "assert": "contains", "value": "<circle cx=\"5\" cy=\"5\" r=\"4\"></circle>" },
{ "path": "blocks.0.innerBlocks.0.innerBlocks.0.attrs.content", "assert": "not_contains", "value": "data:image/svg+xml" },
{ "path": "assets", "assert": "count", "count": 0 },
Expand Down
Loading