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
47 changes: 46 additions & 1 deletion figma-transformer/scripts/figma-fixture-matrix-quality.php
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ function matrix_analyze_dom_box_entrypoint(array $entrypoint): array
$findings[] = matrix_dom_box_finding('dom_viewport_width_leak', $node, array('width' => $width, 'right' => $right, 'viewport_width' => $viewportWidth));
}

if ( $width <= 1.0 || $height <= 1.0 ) {
if ( matrix_dom_box_is_unexpected_collapse($element, $width, $height) ) {
$summary['dom_collapsed_box_count']++;
$findings[] = matrix_dom_box_finding('dom_collapsed_box', $node, array('width' => $width, 'height' => $height));
}
Expand Down Expand Up @@ -481,17 +481,62 @@ function matrix_dom_box_number(array $values, string $key, float $fallback): flo
return isset($values[$key]) && is_numeric($values[$key]) ? (float) $values[$key] : $fallback;
}

/**
* @param array<string, mixed> $element
*/
function matrix_dom_box_is_unexpected_collapse(array $element, float $width, float $height): bool
{
$collapsedWidth = $width <= 1.0;
$collapsedHeight = $height <= 1.0;
if ( ! $collapsedWidth && ! $collapsedHeight ) {
return false;
}

$source = is_array($element['source'] ?? null) ? $element['source'] : array();
$sourceType = isset($source['node_type']) && is_scalar($source['node_type']) ? strtoupper(trim((string) $source['node_type'])) : '';
$isVisibleGeometry = in_array($sourceType, array('LINE', 'VECTOR'), true)
&& true === ($element['visibility']['visible'] ?? null);
if ( ! $isVisibleGeometry || $collapsedWidth === $collapsedHeight ) {
return true;
}

$orthogonalDimension = $collapsedWidth ? $height : $width;
if ( $orthogonalDimension <= 1.0 ) {
return true;
}

$sourceDimensions = is_array($source['visual_dimensions'] ?? null) ? $source['visual_dimensions'] : array();
$collapsedAxis = $collapsedWidth ? 'width' : 'height';
$sourceAxis = isset($sourceDimensions[$collapsedAxis]) && is_numeric($sourceDimensions[$collapsedAxis])
? (float) $sourceDimensions[$collapsedAxis]
: null;
$domAxis = $collapsedWidth ? $width : $height;
if ( null === $sourceAxis || ! is_finite($sourceAxis) || $sourceAxis < 0.0 ) {
return true;
}

if ( abs($sourceAxis - $domAxis) <= 0.5 ) {
return false;
}

// A zero-sized source axis may round up to one visible DOM pixel.
return ! (0.0 === $sourceAxis && $domAxis > 0.0 && $domAxis <= 1.0);
}

/**
* @param array<string, mixed> $element
* @return array<string, mixed>
*/
function matrix_dom_box_node_summary(array $element): array
{
$source = is_array($element['source'] ?? null) ? $element['source'] : array();
return array_filter(array(
'id' => isset($element['node_id']) && is_scalar($element['node_id']) ? (string) $element['node_id'] : null,
'name' => isset($element['node_name']) && is_scalar($element['node_name']) ? (string) $element['node_name'] : null,
'selector' => isset($element['selector']) && is_scalar($element['selector']) ? (string) $element['selector'] : null,
'tag' => isset($element['tag']) && is_scalar($element['tag']) ? (string) $element['tag'] : null,
'source_node_type' => isset($source['node_type']) && is_scalar($source['node_type']) ? (string) $source['node_type'] : null,
'source_visual_dimensions' => is_array($source['visual_dimensions'] ?? null) ? $source['visual_dimensions'] : null,
), static fn (mixed $value): bool => null !== $value && '' !== $value);
}

Expand Down
11 changes: 11 additions & 0 deletions figma-transformer/src/Html/StaticHtmlEmitter.php
Original file line number Diff line number Diff line change
Expand Up @@ -1055,6 +1055,17 @@ private function emitNode(array $node, array &$cssRules, array &$diagnostics, ar
$layoutIntent = $this->layoutIntentClassifier()->layoutIntent($node, $parentNode);
$elementClassName = null === $imageElement ? $className : $className . ' figma-image-asset';
$attributes = sprintf(' class="%1$s" data-figma-node-id="%2$s" data-figma-node-name="%3$s"', $elementClassName, $id, $attributeName);
$attributes .= ' data-source-node-type="' . $this->sanitizeAttribute($type) . '"';
$sourceVisualBox = is_array($node['box'] ?? null) ? $node['box'] : array();
foreach ( array('width', 'height') as $dimension ) {
if ( ! isset($sourceVisualBox[$dimension]) && isset($node[$dimension]) && is_numeric($node[$dimension]) ) {
$sourceVisualBox[$dimension] = $node[$dimension];
}
}
if ( isset($sourceVisualBox['width'], $sourceVisualBox['height']) && is_numeric($sourceVisualBox['width']) && is_numeric($sourceVisualBox['height']) ) {
$attributes .= ' data-source-visual-width="' . $this->sanitizeAttribute((string) $sourceVisualBox['width']) . '"';
$attributes .= ' data-source-visual-height="' . $this->sanitizeAttribute((string) $sourceVisualBox['height']) . '"';
}
$semanticRole = $this->semanticRoleMetadata($node, $tag, $type, $name);
if ( null !== $semanticRole ) {
$attributes .= ' data-figma-semantic-role="' . $this->sanitizeAttribute($semanticRole) . '"';
Expand Down
134 changes: 134 additions & 0 deletions figma-transformer/tests/contract/FixtureMatrixContract.php
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,110 @@ function blocks_engine_figma_transformer_run_fixture_matrix_contract(callable $a
),
),
), '/tmp/dom-boxes.json');
$collapseClassificationQuality = matrix_analyze_dom_box_report(array(
'entrypoints' => array(
array(
'page_path' => '/classification.html',
'viewport' => array('width' => 1440, 'height' => 900),
'dom_css_loaded' => true,
'dom_capture_valid' => true,
'elements' => array(
array(
'node_id' => 'line:separator',
'tag' => 'svg',
'boundingClientRect' => array('left' => 0, 'right' => 240, 'top' => 0, 'bottom' => 1, 'width' => 240, 'height' => 1),
'visibility' => array('visible' => true),
'source' => array('node_type' => 'LINE', 'visual_dimensions' => array('width' => 240, 'height' => 1)),
),
array(
'node_id' => 'vector:tail',
'tag' => 'svg',
'boundingClientRect' => array('left' => 0, 'right' => 0.1, 'top' => 10, 'bottom' => 90, 'width' => 0.1, 'height' => 80),
'visibility' => array('visible' => true),
'source' => array('node_type' => 'VECTOR', 'visual_dimensions' => array('width' => 0.1, 'height' => 80)),
),
array(
'node_id' => 'vector:source-zero-dom-one',
'tag' => 'svg',
'boundingClientRect' => array('left' => 0, 'right' => 943, 'top' => 90, 'bottom' => 91, 'width' => 943, 'height' => 1),
'visibility' => array('visible' => true),
'source' => array('node_type' => 'VECTOR', 'visual_dimensions' => array('width' => 943, 'height' => 0)),
),
array(
'node_id' => 'text:collapsed',
'tag' => 'p',
'boundingClientRect' => array('left' => 0, 'right' => 0, 'top' => 100, 'bottom' => 124, 'width' => 0, 'height' => 24),
'visibility' => array('visible' => false),
'source' => array('node_type' => 'TEXT', 'visual_dimensions' => array('width' => 180, 'height' => 24)),
),
array(
'node_id' => 'frame:collapsed',
'tag' => 'section',
'boundingClientRect' => array('left' => 0, 'right' => 320, 'top' => 130, 'bottom' => 130, 'width' => 320, 'height' => 0),
'visibility' => array('visible' => false),
'source' => array('node_type' => 'FRAME', 'visual_dimensions' => array('width' => 320, 'height' => 120)),
),
array(
'node_id' => 'vector:unexpected-collapse',
'tag' => 'svg',
'boundingClientRect' => array('left' => 0, 'right' => 1, 'top' => 140, 'bottom' => 200, 'width' => 1, 'height' => 60),
'visibility' => array('visible' => true),
'source' => array('node_type' => 'VECTOR', 'visual_dimensions' => array('width' => 8, 'height' => 60)),
),
),
),
),
), '/tmp/dom-box-collapse-classification.json');
$visibleVectorSourceZeroDomOne = array(
'visibility' => array('visible' => true),
'source' => array('node_type' => 'VECTOR', 'visual_dimensions' => array('width' => 943, 'height' => 0)),
);
$invisibleVectorSourceZeroDomOne = $visibleVectorSourceZeroDomOne;
$invisibleVectorSourceZeroDomOne['visibility']['visible'] = false;
$bothAxesCollapsedVector = array(
'visibility' => array('visible' => true),
'source' => array('node_type' => 'VECTOR', 'visual_dimensions' => array('width' => 1, 'height' => 1)),
);
$missingSourceAxisVector = array(
'visibility' => array('visible' => true),
'source' => array('node_type' => 'VECTOR', 'visual_dimensions' => array('width' => 943)),
);
$nonNumericSourceAxisVector = array(
'visibility' => array('visible' => true),
'source' => array('node_type' => 'VECTOR', 'visual_dimensions' => array('width' => 943, 'height' => 'none')),
);
$sourceAxisNotCollapsedVector = array(
'visibility' => array('visible' => true),
'source' => array('node_type' => 'VECTOR', 'visual_dimensions' => array('width' => 943, 'height' => 2)),
);
$sourceAxisOneDomZeroVector = array(
'visibility' => array('visible' => true),
'source' => array('node_type' => 'VECTOR', 'visual_dimensions' => array('width' => 943, 'height' => 1)),
);
$sourceAxisFractionalDomZeroVector = array(
'visibility' => array('visible' => true),
'source' => array('node_type' => 'VECTOR', 'visual_dimensions' => array('width' => 943, 'height' => 0.75)),
);
$sourceAxisWithinToleranceVector = array(
'visibility' => array('visible' => true),
'source' => array('node_type' => 'VECTOR', 'visual_dimensions' => array('width' => 943, 'height' => 0.75)),
);
$negativeSourceAxisVector = array(
'visibility' => array('visible' => true),
'source' => array('node_type' => 'VECTOR', 'visual_dimensions' => array('width' => 943, 'height' => -0.25)),
);
$nanSourceAxisVector = array(
'visibility' => array('visible' => true),
'source' => array('node_type' => 'VECTOR', 'visual_dimensions' => array('width' => 943, 'height' => NAN)),
);
$infiniteSourceAxisVector = array(
'visibility' => array('visible' => true),
'source' => array('node_type' => 'VECTOR', 'visual_dimensions' => array('width' => 943, 'height' => INF)),
);
$ordinaryContentWithCollapsedAxis = array(
'visibility' => array('visible' => true),
'source' => array('node_type' => 'TEXT', 'visual_dimensions' => array('width' => 943, 'height' => 0)),
);
$invalidDomBoxQuality = matrix_analyze_dom_box_report(array(
'schema' => 'homeboy/static-artifact-dom-boxes/v1',
'entrypoints' => array(
Expand Down Expand Up @@ -607,6 +711,36 @@ function blocks_engine_figma_transformer_run_fixture_matrix_contract(callable $a
$assert(1 === ($domBoxQuality['summary']['dom_viewport_width_leak_count'] ?? null), 'fixture-matrix-dom-box-quality-viewport-width-leak');
$assert(1 === ($domBoxQuality['summary']['dom_huge_vertical_spacing_count'] ?? null), 'fixture-matrix-dom-box-quality-huge-vertical-spacing');
$assert(1 === ($domBoxQuality['summary']['dom_collapsed_box_count'] ?? null), 'fixture-matrix-dom-box-quality-collapsed-box');
$assert(3 === ($collapseClassificationQuality['summary']['dom_collapsed_box_count'] ?? null), 'fixture-matrix-dom-box-classification-warns-for-genuine-collapse');
$collapseFindingIds = array_map(
static fn (array $finding): string => (string) ($finding['node']['id'] ?? ''),
$collapseClassificationQuality['pages'][0]['findings'] ?? array()
);
$assert(! in_array('line:separator', $collapseFindingIds, true), 'fixture-matrix-dom-box-classification-allows-visible-one-pixel-line');
$assert(! in_array('vector:tail', $collapseFindingIds, true), 'fixture-matrix-dom-box-classification-allows-source-faithful-subpixel-vector');
$assert(! in_array('vector:source-zero-dom-one', $collapseFindingIds, true), 'fixture-matrix-dom-box-classification-allows-visible-vector-source-zero-dom-one-through-analyzer');
$assert(! matrix_dom_box_is_unexpected_collapse($visibleVectorSourceZeroDomOne, 943, 1), 'fixture-matrix-dom-box-classification-allows-visible-vector-source-zero-dom-one');
$assert(matrix_dom_box_is_unexpected_collapse($invisibleVectorSourceZeroDomOne, 943, 1), 'fixture-matrix-dom-box-classification-warns-for-invisible-vector');
$assert(matrix_dom_box_is_unexpected_collapse($bothAxesCollapsedVector, 1, 1), 'fixture-matrix-dom-box-classification-warns-for-both-axis-collapse');
$assert(matrix_dom_box_is_unexpected_collapse($missingSourceAxisVector, 943, 1), 'fixture-matrix-dom-box-classification-warns-for-missing-source-axis');
$assert(matrix_dom_box_is_unexpected_collapse($nonNumericSourceAxisVector, 943, 1), 'fixture-matrix-dom-box-classification-warns-for-non-numeric-source-axis');
$assert(matrix_dom_box_is_unexpected_collapse($nanSourceAxisVector, 943, 1), 'fixture-matrix-dom-box-classification-warns-for-nan-source-axis');
$assert(matrix_dom_box_is_unexpected_collapse($infiniteSourceAxisVector, 943, 1), 'fixture-matrix-dom-box-classification-warns-for-infinite-source-axis');
$assert(matrix_dom_box_is_unexpected_collapse($negativeSourceAxisVector, 943, 1), 'fixture-matrix-dom-box-classification-warns-for-negative-source-axis');
$assert(matrix_dom_box_is_unexpected_collapse($sourceAxisNotCollapsedVector, 943, 1), 'fixture-matrix-dom-box-classification-warns-for-non-collapsed-source-axis');
$assert(matrix_dom_box_is_unexpected_collapse($sourceAxisOneDomZeroVector, 943, 0), 'fixture-matrix-dom-box-classification-warns-for-source-one-dom-zero');
$assert(matrix_dom_box_is_unexpected_collapse($sourceAxisFractionalDomZeroVector, 943, 0), 'fixture-matrix-dom-box-classification-warns-for-source-fractional-dom-zero');
$assert(! matrix_dom_box_is_unexpected_collapse($sourceAxisWithinToleranceVector, 943, 1), 'fixture-matrix-dom-box-classification-allows-source-within-agreement-tolerance');
$assert(matrix_dom_box_is_unexpected_collapse($ordinaryContentWithCollapsedAxis, 943, 1), 'fixture-matrix-dom-box-classification-warns-for-ordinary-content');
$assert(in_array('text:collapsed', $collapseFindingIds, true), 'fixture-matrix-dom-box-classification-warns-for-collapsed-text');
$assert(in_array('frame:collapsed', $collapseFindingIds, true), 'fixture-matrix-dom-box-classification-warns-for-collapsed-container');
$assert(in_array('vector:unexpected-collapse', $collapseFindingIds, true), 'fixture-matrix-dom-box-classification-warns-for-source-mismatched-vector');
$unexpectedVectorFinding = array_values(array_filter(
$collapseClassificationQuality['pages'][0]['findings'] ?? array(),
static fn (array $finding): bool => 'vector:unexpected-collapse' === ($finding['node']['id'] ?? null)
))[0] ?? array();
$assert('VECTOR' === ($unexpectedVectorFinding['node']['source_node_type'] ?? null), 'fixture-matrix-dom-box-finding-preserves-source-node-type');
$assert(8 === ($unexpectedVectorFinding['node']['source_visual_dimensions']['width'] ?? null), 'fixture-matrix-dom-box-finding-preserves-source-visual-dimensions');
$assert(1 === ($domBoxQuality['summary']['dom_offscreen_box_count'] ?? null), 'fixture-matrix-dom-box-quality-offscreen-box');
$assert(2 === ($domBoxQuality['summary']['dom_missing_node_id_box_count'] ?? null), 'fixture-matrix-dom-box-quality-missing-node-id-boxes');
$assert(false === ($invalidDomBoxQuality['summary']['dom_css_loaded'] ?? null), 'fixture-matrix-invalid-dom-box-css-not-loaded');
Expand Down
Loading
Loading