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
2,772 changes: 2,772 additions & 0 deletions php-transformer/resources/wordpress-6.6-core-block-supports.json

Large diffs are not rendered by default.

69 changes: 69 additions & 0 deletions php-transformer/src/HtmlToBlocks/HtmlTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -3648,6 +3648,7 @@ private function createBlock(string $name, array $attrs = array(), array $innerB
$attrs['className'] = $this->mergeClassNames((string) ($attrs['className'] ?? ''), $nativeButtonMarker);
$this->registerNativeButtonStyleRule($nativeButtonMarker, $hasNativeButtonColor ? $attrs : array(), $nativeButtonTextAlignment);
}
$attrs = $this->applyDeclaredBorderSupport($name, $attrs, $sourceElement);
$provenanceId = $this->nextSourceProvenanceId++;
$this->recordPresentationProvenance($name, $attrs, $sourceElement);
$this->recordStructureProvenance($name, $attrs, $sourceElement);
Expand Down Expand Up @@ -3675,6 +3676,74 @@ private function createBlock(string $name, array $attrs = array(), array $innerB
return $block;
}

/**
* WordPress ignores style.border components that the registered block type
* does not declare. Keep supported components native; move only unsupported
* width/style/color values into the existing deterministic carrier. Border
* radius deliberately stays on the pre-existing native path unchanged.
*
* @param array<string, mixed> $attrs
* @return array<string, mixed>
*/
private function applyDeclaredBorderSupport(string $name, array $attrs, DOMElement $sourceElement): array
{
$border = is_array($attrs['style']['border'] ?? null) ? $attrs['style']['border'] : array();
if ( array() === $border ) {
return $attrs;
}

$fallback = array();
foreach ( array( 'width', 'style', 'color' ) as $component ) {
if ( ! array_key_exists($component, $border) || $this->runtime->blockSupportsBorder($name, $component) ) {
continue;
}
$fallback[ $component ] = $border[ $component ];
unset($border[ $component ]);
}
foreach ( array( 'top', 'right', 'bottom', 'left' ) as $side ) {
$sideBorder = is_array($border[ $side ] ?? null) ? $border[ $side ] : array();
foreach ( array( 'width', 'style', 'color' ) as $component ) {
if ( ! array_key_exists($component, $sideBorder) || $this->runtime->blockSupportsBorder($name, $component) ) {
continue;
}
$fallback[ $side ][ $component ] = $sideBorder[ $component ];
unset($sideBorder[ $component ]);
}
if ( array() === $sideBorder ) {
unset($border[ $side ]);
} else {
$border[ $side ] = $sideBorder;
}
}

if ( array() === $fallback ) {
return $attrs;
}

$fallbackStyle = $this->styleAttributeMapper()->serialize(array( 'border' => $fallback ))['style'];
$fallbackDeclarations = $this->cssDeclarations($fallbackStyle);
$carrier = $this->inlineGeometryClassName(
$sourceElement,
array(),
array_keys($fallbackDeclarations),
$fallbackDeclarations
);
if ( '' !== $carrier ) {
$attrs['className'] = $this->mergeClassNames((string) ($attrs['className'] ?? ''), $carrier);
}

if ( array() === $border ) {
unset($attrs['style']['border']);
} else {
$attrs['style']['border'] = $border;
}
if ( empty($attrs['style']) ) {
unset($attrs['style']);
}

return $attrs;
}

/**
* Project the inherited foreground because core/button supplies a default link
* color. Text alignment uses the same scoped link rule for direct and inherited
Expand Down
178 changes: 149 additions & 29 deletions php-transformer/src/HtmlToBlocks/Style/StyleAttributeMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,24 @@ public function serialize(array $style): array
if ( '' !== trim((string) ($border['radius'] ?? '')) ) {
$declarations[] = 'border-radius:' . trim((string) $border['radius']);
}
// No class for a per-side color. The core style engine gives `classnames`
// to the uniform `border.color` definition only, and `has-border-color`
// is an all-sides signal: core's block-library `common.css` ships
// `html :where(.has-border-color){border-style:solid}`, which would paint
// the three unauthored sides at the initial `medium` width in
// `currentColor` and grow the box by 6px.
foreach ( array( 'top', 'right', 'bottom', 'left' ) as $side ) {
$sideBorder = is_array($border[ $side ] ?? null) ? $border[ $side ] : array();
if ( '' !== trim((string) ($sideBorder['color'] ?? '')) ) {
$declarations[] = 'border-' . $side . '-color:' . trim((string) $sideBorder['color']);
}
if ( '' !== trim((string) ($sideBorder['style'] ?? '')) ) {
$declarations[] = 'border-' . $side . '-style:' . trim((string) $sideBorder['style']);
}
if ( '' !== trim((string) ($sideBorder['width'] ?? '')) ) {
$declarations[] = 'border-' . $side . '-width:' . trim((string) $sideBorder['width']);
}
}

// Match the core style engine: dimensions precede spacing in save().
$dimensions = is_array($style['dimensions'] ?? null) ? $style['dimensions'] : array();
Expand Down Expand Up @@ -448,64 +466,166 @@ private function boxSides(string $property, array $declarations, array &$consume
private function border(array $declarations, array &$consumed): array
{
$border = array();
$shorthand = $this->parseBorderShorthand((string) ($declarations['border'] ?? ''));
if ( isset($declarations['border']) ) {
$consumed['border'] = true;
}

$width = trim((string) ($declarations['border-width'] ?? $shorthand['width'] ?? ''));
if ( '' === $width ) {
$width = $this->uniformBorderSideValue('width', $declarations, $consumed);
}
$style = strtolower(trim((string) ($declarations['border-style'] ?? $shorthand['style'] ?? '')));
$colorValue = $this->cssColor((string) ($declarations['border-color'] ?? $shorthand['color'] ?? ''));
foreach ( array( 'border-width', 'border-style', 'border-color' ) as $name ) {
if ( isset($declarations[ $name ]) ) {
$positions = array_flip(array_keys($declarations));
foreach ( array_keys($declarations) as $name ) {
if ( $this->isMappedBorderProperty($name) ) {
$consumed[ $name ] = true;
}
}

$global = array();
foreach ( array( 'width', 'style', 'color' ) as $component ) {
$global[ $component ] = $this->borderComponentCandidate($declarations, $positions, $component);
}

$width = trim($global['width']['value']);
$style = strtolower(trim($global['style']['value']));
$colorValue = $this->cssColor($global['color']['value']);

$noBorder = 'none' === $style || ( '' !== $width && (float) $width === 0.0 && '' === $colorValue && '' === $style );
if ( ! $noBorder ) {
if ( '' !== $width && (float) $width !== 0.0 ) {
if ( $global['width']['declared'] && '' !== $width && (float) $width !== 0.0 ) {
$border['width'] = $width;
}
if ( '' !== $style && 'none' !== $style ) {
if ( $global['style']['declared'] && '' !== $style && 'none' !== $style ) {
$border['style'] = $style;
}
if ( '' !== $colorValue ) {
if ( $global['color']['declared'] && '' !== $colorValue ) {
$border['color'] = $colorValue;
}
}
$hasGlobalBorder = isset($border['width']) || isset($border['style']) || isset($border['color']);

$radius = trim((string) ($declarations['border-radius'] ?? ''));
if ( '' !== $radius ) {
$consumed['border-radius'] = true;
$border['radius'] = $radius;
$border['radius'] = $radius;
}

foreach ( array( 'top', 'right', 'bottom', 'left' ) as $side ) {
$sideComponents = array();
$sideDeclared = array();
foreach ( array( 'width', 'style', 'color' ) as $component ) {
$candidate = $this->borderComponentCandidate($declarations, $positions, $component, $side);
if ( $candidate['index'] > $global[ $component ]['index'] ) {
$sideComponents[ $component ] = $candidate['value'];
$sideDeclared[ $component ] = $candidate['declared'];
}
}

$sideWidth = trim((string) ($sideComponents['width'] ?? ''));
$sideStyle = strtolower(trim((string) ($sideComponents['style'] ?? '')));
$sideColor = $this->cssColor((string) ($sideComponents['color'] ?? ''));

$sideValues = array();
$noSideBorder = 'none' === $sideStyle || ( '' !== $sideWidth && (float) $sideWidth === 0.0 && '' === $sideColor && '' === $sideStyle );
if ( ! $noSideBorder || $hasGlobalBorder ) {
if ( ( ($sideDeclared['width'] ?? false) || $hasGlobalBorder ) && '' !== $sideWidth && ( (float) $sideWidth !== 0.0 || $hasGlobalBorder ) ) {
$sideValues['width'] = $sideWidth;
}
if ( ( ($sideDeclared['style'] ?? false) || $hasGlobalBorder ) && '' !== $sideStyle && ( 'none' !== $sideStyle || $hasGlobalBorder ) ) {
$sideValues['style'] = $sideStyle;
}
if ( ( ($sideDeclared['color'] ?? false) || $hasGlobalBorder ) && '' !== $sideColor ) {
$sideValues['color'] = $sideColor;
}
}
if ( array() !== $sideValues ) {
$border[ $side ] = $sideValues;
}
}

$this->collapseUniformBorderSideComponent($border, 'width');

return $border;
}

/**
* Collapse equal physical side values into the canonical border support.
* Unequal sides remain under author stylesheet ownership.
* Return the last authored global or per-side value for one border component.
* A shorthand participates even when it omits the component because CSS
* shorthands reset omitted values to their initial state. Such a substituted
* initial value is reported as `declared: false`: it settles precedence and
* cancels a border this mapper itself emits, but it is never authored, so
* callers must not serialize it. Materializing one would place an inline
* declaration the author never wrote above their own state rules — a
* `border: 2px solid transparent` base plus a `:hover { border-color }` rule
* would freeze at `currentColor`.
*
* @param array<string, string> $declarations
* @param array<string, bool> $consumed
* @param array<string, int> $positions
* @return array{value: string, index: int, declared: bool}
*/
private function uniformBorderSideValue(string $property, array $declarations, array &$consumed): string
private function borderComponentCandidate(array $declarations, array $positions, string $component, string $side = ''): array
{
$names = array_map(static fn (string $side): string => 'border-' . $side . '-' . $property, array( 'top', 'right', 'bottom', 'left' ));
$values = array_map(static fn (string $name): string => trim((string) ($declarations[ $name ] ?? '')), $names);
if ( in_array('', $values, true) || 1 !== count(array_unique($values)) ) {
return '';
$shorthandName = '' === $side ? 'border' : 'border-' . $side;
$longhandName = $shorthandName . '-' . $component;
$candidate = array( 'value' => '', 'index' => -1, 'declared' => false );

if ( isset($declarations[ $shorthandName ]) ) {
$shorthand = $this->parseBorderShorthand($declarations[ $shorthandName ]);
$initialValues = array(
'width' => 'medium',
'style' => 'none',
'color' => 'currentColor',
);
$authored = array() !== $shorthand && isset($shorthand[ $component ]);
$candidate = array(
'value' => array() === $shorthand ? '' : (string) ($shorthand[ $component ] ?? $initialValues[ $component ]),
'index' => $positions[ $shorthandName ],
'declared' => $authored,
);
}

if ( isset($declarations[ $longhandName ]) && $positions[ $longhandName ] > $candidate['index'] ) {
$candidate = array(
'value' => $declarations[ $longhandName ],
'index' => $positions[ $longhandName ],
'declared' => true,
);
}

return $candidate;
}

private function isMappedBorderProperty(string $name): bool
{
if ( in_array($name, array( 'border', 'border-width', 'border-style', 'border-color', 'border-radius' ), true) ) {
return true;
}

return (bool) preg_match('/^border-(?:top|right|bottom|left)(?:-(?:width|style|color))?$/', $name);
}

/**
* Collapse four equal physical side values into one canonical component and
* remove the now-redundant side objects.
*
* @param array<string, mixed> $border
*/
private function collapseUniformBorderSideComponent(array &$border, string $component): void
{
$sides = array( 'top', 'right', 'bottom', 'left' );
$values = array();
foreach ( $sides as $side ) {
$sideBorder = is_array($border[ $side ] ?? null) ? $border[ $side ] : array();
$value = trim((string) ($sideBorder[ $component ] ?? ''));
if ( '' === $value ) {
return;
}
$values[] = $value;
}

foreach ( $names as $name ) {
$consumed[ $name ] = true;
if ( 1 !== count(array_unique($values)) ) {
return;
}

foreach ( $sides as $side ) {
unset($border[ $side ][ $component ]);
if ( array() === $border[ $side ] ) {
unset($border[ $side ]);
}
}
return $values[0];
unset($border[ $component ]);
$border = array( $component => $values[0] ) + $border;
}

/**
Expand Down
16 changes: 16 additions & 0 deletions php-transformer/src/HtmlToBlocks/Style/StyleResolutionTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -1695,14 +1695,26 @@ private function safeVisualDeclarations(array $declarations): array
'background-size',
'aspect-ratio',
'border',
'border-bottom',
'border-bottom-color',
'border-bottom-style',
'border-color',
'border-left',
'border-left-color',
'border-left-style',
'border-radius',
'border-right',
'border-right-color',
'border-right-style',
'border-style',
'border-bottom-width',
'border-collapse',
'border-left-width',
'border-right-width',
'border-spacing',
'border-top',
'border-top-color',
'border-top-style',
'border-top-width',
'border-width',
'box-shadow',
Expand Down Expand Up @@ -1775,6 +1787,10 @@ private function cssDeclarations(string $style): array
$value = preg_replace('/\s+/', ' ', $value) ?? $value;
$allowsImageUrl = in_array($name, array( 'background', 'background-image', 'list-style', 'list-style-image' ), true) && ! preg_match('/(?:expression\s*\(|javascript\s*:)/i', $value);
if ( '' !== $name && '' !== $value && ( $allowsImageUrl || ! preg_match('/(?:expression\s*\(|javascript\s*:|url\s*\()/i', $value) ) ) {
// Keep the surviving declaration at its final authored position.
// Border shorthands and longhands reset one another in source
// order, so overwriting a prior key in place is not sufficient.
unset($declarations[$name]);
$declarations[$name] = $value;
}
}
Expand Down
Loading
Loading