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
14 changes: 13 additions & 1 deletion figma-transformer/src/FigmaTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -1551,6 +1551,10 @@ private function mergePageTransformDiagnostics(array $pageReports, array $assetR
'invalid_numeric_token_count' => 0,
'invalid_numeric_tokens' => array(),
);
$textCoverage = array(
'empty_decoded_text_nodes' => 0,
'missing_emitted_text_nodes' => 0,
);
$htmlArtifact = array(
'schema' => 'blocks-engine/figma-transformer/html-artifact-diagnostics/v1',
'media_query_count' => 0,
Expand Down Expand Up @@ -1629,6 +1633,8 @@ private function mergePageTransformDiagnostics(array $pageReports, array $assetR
$pageCss = is_array($diagnostics['css'] ?? null) ? $diagnostics['css'] : array();
DiagnosticAggregation::addIntegerCounts($css, $pageCss, array('invalid_numeric_token_count'));
DiagnosticAggregation::appendContextSamples($css, 'invalid_numeric_tokens', $pageCss, 'invalid_numeric_tokens', $pageContext);
$pageQualitySummary = is_array($diagnostics['artifact_quality']['summary'] ?? null) ? $diagnostics['artifact_quality']['summary'] : array();
DiagnosticAggregation::addIntegerCounts($textCoverage, $pageQualitySummary, array('empty_decoded_text_nodes', 'missing_emitted_text_nodes'));
$pageHtmlArtifact = is_array($diagnostics['html_artifact'] ?? null) ? $diagnostics['html_artifact'] : array();
DiagnosticAggregation::addIntegerCounts($htmlArtifact, $pageHtmlArtifact, array('media_query_count', 'fixed_width_over_desktop_count', 'fixed_width_declaration_count', 'fixed_width_with_responsive_override_count', 'fixed_width_without_responsive_override_count', 'giant_fixed_section_count', 'large_overflow_risk_count', 'fallback_prone_form_island_count', 'fallback_prone_svg_island_count', 'fallback_prone_input_island_count', 'invalid_list_child_count', 'missing_semantic_role_count'));
DiagnosticAggregation::appendContextSamples($htmlArtifact, 'fixed_width_samples', $pageHtmlArtifact, 'fixed_width_samples', $pageContext);
Expand Down Expand Up @@ -1777,6 +1783,12 @@ private function mergePageTransformDiagnostics(array $pageReports, array $assetR
);
$generatedSvgAssets = $this->generatedSvgAssetsFromReport($assetReport);

$artifactQuality = $this->artifactQualityDiagnostics($images, $vectors, $fonts, $assets, $generatedSvgAssets, $layout, $links, $css, $htmlArtifact);
$artifactQuality['summary'] = array_merge(
is_array($artifactQuality['summary'] ?? null) ? $artifactQuality['summary'] : array(),
$textCoverage
);

return array(
'schema' => 'blocks-engine/figma-transformer/transform-diagnostics/v1',
'scope' => 'multi_page',
Expand All @@ -1793,7 +1805,7 @@ private function mergePageTransformDiagnostics(array $pageReports, array $assetR
'links' => $links,
'css' => $css,
'html_artifact' => $htmlArtifact,
'artifact_quality' => $this->artifactQualityDiagnostics($images, $vectors, $fonts, $assets, $generatedSvgAssets, $layout, $links, $css, $htmlArtifact),
'artifact_quality' => $artifactQuality,
'diagnostic_codes' => $diagnosticCodes,
);
}
Expand Down
11 changes: 10 additions & 1 deletion figma-transformer/src/Html/StaticHtmlEmitter.php
Original file line number Diff line number Diff line change
Expand Up @@ -4716,6 +4716,9 @@ private function componentCloneOmissionReason(array $node, ?array $parentNode, ?
if ( null !== $parentNode && $this->isComposedVectorChild($node, $parentNode) ) {
return 'composed-into-parent';
}
if ( null !== $parentNode && $this->formControlComposesChild($parentNode, $node) ) {
return 'form-control-composed-into-parent';
}
if ( $this->isComponentSourceDuplicateNode($node) ) {
return 'component_source_duplicate';
}
Expand All @@ -4735,7 +4738,13 @@ private function componentCloneOmissionReason(array $node, ?array $parentNode, ?

private function isIntentionalComponentCloneSuppression(string $reason): bool
{
return in_array($reason, array('hidden', 'zero-area', 'mask-source', 'composed-into-parent', 'non-rendering-vector-layer', 'invisible-zero-area-scaffold', 'component_source_duplicate'), true);
return in_array($reason, array('hidden', 'zero-area', 'mask-source', 'composed-into-parent', 'form-control-composed-into-parent', 'non-rendering-vector-layer', 'invisible-zero-area-scaffold', 'component_source_duplicate'), true);
}

private function formControlComposesChild(array $parent, array $child): bool
{
$parentIsControl = ($this->isInputLike($parent) || $this->isTextareaLike($parent)) && $this->hasFormControlAccessoryChildren($parent);
return $parentIsControl && ($this->isFormControlPlaceholderChild($child) || $this->isInputLike($child, $parent) || $this->isTextareaLike($child, $parent));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -944,6 +944,8 @@ function blocks_engine_figma_transformer_run_diagnostics_evidence_contract(calla
$assert('diag:aggregation-home-vector' === ($placeholderNodes[0]['node_id'] ?? null), 'diagnostics-evidence-multi-page-placeholder-home-node');
$assert('aggregation-about.html' === ($placeholderNodes[1]['page_path'] ?? null), 'diagnostics-evidence-multi-page-placeholder-about-context');
$assert(2 === ($multiPageDiagnostics['diagnostic_codes']['unsupported_vector_node_placeholder'] ?? null), 'diagnostics-evidence-multi-page-diagnostic-code-count');
$assert(0 === ($multiPageDiagnostics['artifact_quality']['summary']['empty_decoded_text_nodes'] ?? null), 'diagnostics-evidence-multi-page-empty-text-count-aggregated');
$assert(0 === ($multiPageDiagnostics['artifact_quality']['summary']['missing_emitted_text_nodes'] ?? null), 'diagnostics-evidence-multi-page-missing-text-count-aggregated');
blocks_engine_figma_transformer_contract_assert_diagnostic_value($assert, $multiPageDiagnostics, array('decision_traces', 'schema'), 'blocks-engine/figma-transformer/decision-traces/v1', 'diagnostics-evidence-multi-page-decision-traces-schema');
$assert(2 <= ($multiPageDiagnostics['decision_traces']['domain_counts']['positioning_context'] ?? 0), 'diagnostics-evidence-multi-page-decision-traces-positioning-aggregated');
blocks_engine_figma_transformer_contract_assert_diagnostic_value($assert, $multiPageDiagnostics, array('layout', 'positional_parity', 'schema'), 'blocks-engine/figma-transformer/positional-parity/v1', 'diagnostics-evidence-multi-page-positional-parity-schema');
Expand Down
19 changes: 19 additions & 0 deletions php-transformer/src/ArtifactCompiler/ArtifactCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,9 @@ private function runtimeDeclarationsFromFallbacks(array $declarations, array $fa
}
}
ksort($products, SORT_STRING); ksort($forms, SORT_STRING);
$bindingOccurrences = array();
$products = $this->normalizeEntityBindingOccurrences($products, $bindingOccurrences);
$forms = $this->normalizeEntityBindingOccurrences($forms, $bindingOccurrences);

$collections = array(
'shop' => array('type' => 'products', 'aliases' => array('product', 'products'), 'entities' => array_values($products), 'schema' => 'generic/products/v1'),
Expand All @@ -305,6 +308,22 @@ private function runtimeDeclarationsFromFallbacks(array $declarations, array $fa
return RuntimeDeclarations::normalizeList($declarations);
}

/** @param array<string,array<string,mixed>> $entities @param array<string,int> $occurrences @return array<string,array<string,mixed>> */
private function normalizeEntityBindingOccurrences(array $entities, array &$occurrences): array
{
foreach ($entities as &$entity) {
if (!is_array($entity['bindings'] ?? null)) continue;
foreach ($entity['bindings'] as &$binding) {
$key = ($binding['source_path'] ?? '') . "\n" . ($binding['role'] ?? '') . "\n" . ($binding['search_block_markup'] ?? '');
$occurrences[$key] = ($occurrences[$key] ?? 0) + 1;
$binding['occurrence'] = $occurrences[$key];
}
unset($binding);
}
unset($entity);
return $entities;
}

/** @param array<string,mixed> $fallback @param array<int,array<string,mixed>> $files @return array<int,array<string,string>> */
private function supersededFormScripts(array $fallback, array $files, string $sourcePath): array
{
Expand Down
20 changes: 15 additions & 5 deletions php-transformer/src/HtmlToBlocks/BlockFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -422,8 +422,14 @@ private function imageHtml(array $attrs): string
{
$figureAttrs = $attrs;
$baseClass = 'wp-block-image';
if ( ! empty($attrs['style']['border']) ) {
$baseClass .= ' has-custom-border';
$border = is_array($attrs['style']['border'] ?? null) ? $attrs['style']['border'] : array();
$borderSupport = $this->styleSupport(array( 'border' => $border ));
if ( array() !== $border ) {
$baseClass = $this->mergeClassNames('wp-block-image has-custom-border', $borderSupport['classes']);
unset($figureAttrs['style']['border']);
if ( empty($figureAttrs['style']) ) {
unset($figureAttrs['style']);
}
}
if ( ! empty($attrs['sizeSlug']) ) {
$figureAttrs['className'] = $this->mergeClassNames((string) ($figureAttrs['className'] ?? ''), 'size-' . (string) $attrs['sizeSlug']);
Expand All @@ -438,8 +444,8 @@ private function imageHtml(array $attrs): string
'title' => $attrs['title'] ?? '',
'srcset' => $attrs['srcset'] ?? '',
'sizes' => $attrs['sizes'] ?? '',
'class' => ! empty($attrs['id']) ? 'wp-image-' . (string) $attrs['id'] : '',
'style' => $this->imageDimensionStyle($attrs),
'class' => $this->mergeClassNames(! empty($attrs['id']) ? 'wp-image-' . (string) $attrs['id'] : '', $borderSupport['classes']),
'style' => trim($this->imageDimensionStyle($attrs) . ';' . $borderSupport['style'], ';'),
);

$img = '<img' . $this->htmlAttrs($imageAttrs, array( 'alt' )) . '/>';
Expand Down Expand Up @@ -481,11 +487,15 @@ private function galleryClasses(array $attrs): string
*/
private function imageDimensionStyle(array $attrs): string
{
if ( ! array_key_exists('width', $attrs) && ! array_key_exists('height', $attrs) ) {
if ( ! array_key_exists('width', $attrs) && ! array_key_exists('height', $attrs) && ! array_key_exists('scale', $attrs) ) {
return '';
}

$style = array();
if ( array_key_exists('scale', $attrs) && null !== $attrs['scale'] ) {
$style[] = 'object-fit:' . (string) $attrs['scale'];
}

if ( array_key_exists('width', $attrs) && null !== $attrs['width'] ) {
$style[] = 'width:' . (string) $attrs['width'];
}
Expand Down
78 changes: 76 additions & 2 deletions php-transformer/src/HtmlToBlocks/HtmlTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,9 @@ final class HtmlTransformer
/** @var array<string,int> */
private array $blockBindingOccurrences = array();

/** @var array<string,true> */
private array $formControlSlotPaths = array();

/**
* @var array<int, array<string, mixed>>
*/
Expand Down Expand Up @@ -431,6 +434,7 @@ public function transform(string $html, array $options = array()): TransformerRe
$this->droppedLinkWrapperFindings = array();
$this->sourceProvenance = array();
$this->blockBindingOccurrences = array();
$this->formControlSlotPaths = array();
$this->structureProvenance = array();
$this->scriptMetadata = array();
$this->runtimeIslands = array();
Expand Down Expand Up @@ -1215,7 +1219,10 @@ private function imageProjectionBridgeDeclarations(array $declarations): string
{
$bridge = array( 'display:block' );
$position = strtolower(trim((string) ($declarations['position'] ?? '')));
if ( in_array($position, array( 'absolute', 'fixed' ), true) ) {
$width = strtolower(trim((string) ($declarations['width'] ?? '')));
$height = strtolower(trim((string) ($declarations['height'] ?? '')));
$ownsBox = ! in_array($width, array( '', 'auto' ), true) && ! in_array($height, array( '', 'auto' ), true);
if ( $ownsBox || in_array($position, array( 'absolute', 'fixed' ), true) ) {
$bridge[] = 'width:100%';
$bridge[] = 'height:100%';
}
Expand Down Expand Up @@ -1653,6 +1660,10 @@ private function convertElement(DOMElement $element, array &$fallbacks, bool $ca
{
$tagName = strtolower($element->tagName);

if ( isset($this->formControlSlotPaths[$element->getNodePath()]) ) {
return $this->htmlPreservationBlock($element);
}

if ( $this->isRedundantMenuToggleControl($element) ) {
return null;
}
Expand Down Expand Up @@ -6469,6 +6480,58 @@ private function readableFormBlockFromForm(DOMElement $form, bool $allowFormEven
return $this->createBlock('core/group', $this->presentationAttributes($form), $contentBlocks, $form);
}

/**
* Preserve one unambiguous controls-only subtree as the provider binding
* slot while converting the form's surrounding visual content normally.
*
* @param array<int,array<string,mixed>> $fallbacks
* @return array{block:array<string,mixed>,slot:array<string,mixed>}|null
*/
private function compositionalFormBlock(DOMElement $form, array &$fallbacks): ?array
{
$slot = $this->formControlSlotElement($form);
if ( null === $slot ) return null;

$path = $slot->getNodePath();
$this->formControlSlotPaths[$path] = true;
try {
$children = $this->convertChildren($form, $fallbacks, true);
} finally {
unset($this->formControlSlotPaths[$path]);
}
if ( array() === $children ) return null;

return array(
'block' => $this->createBlock('core/group', $this->presentationAttributes($form), $children, $form),
'slot' => $this->htmlPreservationBlock($slot),
);
}

private function formControlSlotElement(DOMElement $form): ?DOMElement
{
$controls = $this->formControlElements($form);
if ( array() === $controls ) return null;

$formPath = $form->getNodePath();
for ( $candidate = $controls[0]->parentNode; $candidate instanceof DOMElement && $candidate->getNodePath() !== $formPath; $candidate = $candidate->parentNode ) {
if ( array_filter($controls, fn(DOMElement $control): bool => !$this->elementContains($candidate, $control)) ) continue;
foreach ( $candidate->childNodes as $child ) {
if ( XML_TEXT_NODE === $child->nodeType && '' !== trim($child->textContent ?? '') ) continue 2;
if ( !$child instanceof DOMElement ) continue;
if ( !array_filter($controls, fn(DOMElement $control): bool => $this->elementContains($child, $control)) ) continue 2;
}
return $candidate;
}
return null;
}

private function elementContains(DOMElement $ancestor, DOMElement $element): bool
{
$ancestorPath = $ancestor->getNodePath();
for ( $node = $element; $node instanceof DOMElement; $node = $node->parentNode ) if ( $node->getNodePath() === $ancestorPath ) return true;
return false;
}

/**
* @return array<string, mixed>|null
*/
Expand Down Expand Up @@ -6732,8 +6795,11 @@ private function formFallbackFinding(DOMElement $element, ?array $readableFormBl
{
$controls = $this->formControls($element);
$boundedHtml = $this->boundedFallbackHtml($this->safeFallbackHtml($element));
$replacesRuntimeIsland = null !== $bindingBlock;
$bindingBlock ??= $readableFormBlock;
$bindingMarkup = null !== $bindingBlock ? $this->runtime->serializeBlocks(array($bindingBlock)) : '';
$supersededRuntimeSelectors = $this->runtimeDomSelectorsForElement($element);
if ( $replacesRuntimeIsland ) $supersededRuntimeSelectors[] = $this->runtimeIslandSelector($element);

return FallbackDiagnostic::build(array(
'type' => 'html',
Expand All @@ -6750,7 +6816,7 @@ private function formFallbackFinding(DOMElement $element, ?array $readableFormBl
'classification' => $this->fallbackEmitter->classifyFallbackSubtree($element),
'events' => $this->eventMetadata($element),
'readable_blocks' => null !== $readableFormBlock ? array( $readableFormBlock ) : array(),
'binding' => $this->blockBinding($bindingMarkup, 'form', $this->runtimeDomSelectorsForElement($element)),
'binding' => $this->blockBinding($bindingMarkup, 'form', $supersededRuntimeSelectors),
'controls' => $controls,
'control_count' => count($controls),
'text_length' => strlen(trim($element->textContent ?? '')),
Expand Down Expand Up @@ -7369,15 +7435,23 @@ private function selectOptions(DOMElement $select): array
*/
private function backgroundImageBlockFromElement(DOMElement $element): ?array
{
$declarations = $this->presentationDeclarations($element);
$url = $this->backgroundImageExtractor->urlFromStyle($this->mergedPresentationStyle($element));
if ( '' === $url ) {
return null;
}

$width = trim((string) ($declarations['width'] ?? ''));
$height = trim((string) ($declarations['height'] ?? ''));
$scale = strtolower(trim((string) ($declarations['background-size'] ?? '')));

return $this->createBlock('core/image', array_filter(array(
'url' => $this->resolvedAssetImageUrl($url),
'alt' => $this->backgroundImageExtractor->altFromAttributes($this->htmlAttributes($element)),
'className' => 'blocks-engine-background-image',
'width' => ! in_array(strtolower($width), array( '', 'auto' ), true) ? $width : '',
'height' => ! in_array(strtolower($height), array( '', 'auto' ), true) ? $height : '',
'scale' => in_array($scale, array( 'cover', 'contain' ), true) ? $scale : '',
), static fn (string $value): bool => '' !== $value), array(), $element);
}

Expand Down
24 changes: 24 additions & 0 deletions php-transformer/src/HtmlToBlocks/Style/StyleAttributeMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,9 @@ private function border(array $declarations, array &$consumed): array
}

$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 ) {
Expand Down Expand Up @@ -483,6 +486,27 @@ private function border(array $declarations, array &$consumed): array
return $border;
}

/**
* Collapse equal physical side values into the canonical border support.
* Unequal sides remain under author stylesheet ownership.
*
* @param array<string, string> $declarations
* @param array<string, bool> $consumed
*/
private function uniformBorderSideValue(string $property, array $declarations, array &$consumed): string
{
$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 '';
}

foreach ( $names as $name ) {
$consumed[ $name ] = true;
}
return $values[0];
}

/**
* @return array{width?: string, style?: string, color?: string}
*/
Expand Down
Loading
Loading