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 changes: 1 addition & 1 deletion php-transformer/src/Contract/ConversionFindingContract.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ final class ConversionFindingContract
*/
private const ARRAY_FIELDS = array(
'source_selector_specificity', 'context', 'events', 'classification',
'controls', 'control', 'form', 'readable_blocks', 'signals',
'controls', 'control', 'form', 'readable_blocks', 'signals', 'materialization_target', 'products',
'source_items', 'block_items', 'source_item', 'block_item',
);

Expand Down
4 changes: 4 additions & 0 deletions php-transformer/src/Contract/ConversionReportProjection.php
Original file line number Diff line number Diff line change
Expand Up @@ -145,21 +145,25 @@ private static function fallbackDiagnostics(array $fallbacks): array
'pattern_family' => $fallback['pattern_family'] ?? '',
'pattern_family_detail' => $fallback['pattern_family_detail'] ?? '',
'materialization_hint' => $fallback['materialization_hint'] ?? '',
'materialization_target' => $fallback['materialization_target'] ?? array(),
'source_format' => $fallback['source_format'] ?? '',
'source' => $fallback['source'] ?? '',
'scope' => $fallback['scope'] ?? '',
'source_path' => $fallback['source_path'] ?? '',
'tag' => $fallback['tag'] ?? '',
'selector' => $fallback['selector'] ?? '',
'container_selector' => $fallback['container_selector'] ?? '',
'source_selector' => $fallback['source_selector'] ?? '',
'source_selector_specificity' => $fallback['source_selector_specificity'] ?? array(),
'parent_reason' => $fallback['parent_reason'] ?? '',
'ancestor_reason' => $fallback['ancestor_reason'] ?? '',
'child_count' => $fallback['child_count'] ?? null,
'control_count' => $fallback['control_count'] ?? null,
'product_count' => $fallback['product_count'] ?? null,
'form' => $fallback['form'] ?? array(),
'control' => $fallback['control'] ?? array(),
'controls' => $fallback['controls'] ?? array(),
'products' => $fallback['products'] ?? array(),
'context' => $fallback['context'] ?? array(),
'events' => $fallback['events'] ?? array(),
'script_dependency_hint' => $fallback['script_dependency_hint'] ?? '',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ public function collect(
'suggested_repair_class' => $fallback['suggested_repair_class'] ?? null,
'suggested_primitive' => $fallback['suggested_primitive'] ?? null,
'materialization_hint' => $fallback['materialization_hint'] ?? null,
'materialization_target' => $fallback['materialization_target'] ?? null,
'tag' => $fallback['tag'] ?? null,
'selector' => $fallback['selector'] ?? null,
'pattern_family' => $fallback['pattern_family'] ?? null,
Expand Down
43 changes: 38 additions & 5 deletions php-transformer/src/HtmlToBlocks/FallbackDiagnostic.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public static function withGenericFindingMetadata(array $fields): array
private static function defaults(array $fields): array
{
$code = (string) ($fields['diagnostic_code'] ?? '');
$formHasControls = 'html_form_fallback' === $code && ! empty($fields['controls']) && is_array($fields['controls']);

return match ( $code ) {
'html_form_fallback' => array(
Expand All @@ -60,11 +61,17 @@ private static function defaults(array $fields): array
'diagnostic_class' => 'runtime_island_preserved',
'preservation_strategy' => 'fallback_metadata_with_readable_blocks',
'runtime_requirement' => 'server_or_client_form_handler',
'recoverability' => 'recoverable_with_runtime_mapping',
'actionability' => 'map_form_action_controls_and_submission_handler',
'suggested_repair_class' => 'preserve_runtime_island',
'recoverability' => $formHasControls ? 'recoverable_with_form_provider_materialization' : 'recoverable_with_runtime_mapping',
'actionability' => $formHasControls ? 'materialize_detected_form_with_form_provider' : 'map_form_runtime_or_preserve_handler',
'suggested_repair_class' => $formHasControls ? 'materialize_form_provider' : 'preserve_runtime_island',
'suggested_primitive' => 'form',
'materialization_hint' => 'preserve_form_markup_or_replace_with_form_block_integration',
'materialization_hint' => $formHasControls ? 'map_form_action_controls_labels_options_and_submit_text_to_a_form_provider' : 'preserve_form_runtime_source_until_controls_can_be_mapped',
'materialization_target' => $formHasControls ? array(
'capability' => 'form',
'entity' => 'form',
'provider_role' => 'form_provider',
'requires' => array( 'controls', 'form' ),
) : array(),
),
'html_product_grid_fallback' => array(
'severity' => 'info',
Expand All @@ -77,7 +84,32 @@ private static function defaults(array $fields): array
'actionability' => 'materialize_detected_products_in_a_commerce_provider',
'suggested_repair_class' => 'materialize_commerce_products',
'suggested_primitive' => 'product_grid',
'materialization_hint' => 'layout_blocks_are_emitted_as_is; map_each_detected_product_name_price_and_cart_control_onto_a_commerce_provider',
'materialization_hint' => 'layout_blocks_are_emitted_as_is; map_detected_product_names_prices_images_and_descriptions_to_a_shop_provider',
'materialization_target' => array(
'capability' => 'shop',
'entity' => 'product',
'provider_role' => 'commerce_product_provider',
'requires' => array( 'products' ),
),
),
'html_commerce_controls_fallback' => array(
'severity' => 'warning',
'conversion_classification' => 'runtime_island_preserved',
'loss_class' => 'runtime_island_preserved',
'diagnostic_class' => 'commerce_runtime_controls_detected',
'preservation_strategy' => 'layout_blocks_with_commerce_control_metadata',
'runtime_requirement' => 'commerce_cart_runtime',
'recoverability' => 'recoverable_with_commerce_cart_runtime_binding',
'actionability' => 'bind_quantity_and_add_to_cart_controls_to_cart_runtime_after_product_materialization',
'suggested_repair_class' => 'materialize_commerce_runtime',
'suggested_primitive' => 'commerce_controls',
'materialization_hint' => 'product_data_can_be_seeded_by_a_shop_provider; core_blocks_cannot_provide_cart_state_or_quantity_mutation_without_runtime_binding',
'materialization_target' => array(
'capability' => 'shop',
'entity' => 'commerce_controls',
'provider_role' => 'commerce_cart_runtime',
'requires' => array( 'controls', 'seeded_products' ),
),
),
'html_script_fallback' => array(
'severity' => 'warning',
Expand Down Expand Up @@ -225,6 +257,7 @@ private static function patternFamily(array $fields): string
return match ( $code ) {
'html_form_fallback' => 'interactive_form',
'html_product_grid_fallback' => 'commerce_product_grid',
'html_commerce_controls_fallback' => 'commerce_controls',
'html_script_fallback' => 'runtime_script',
'interactive_control_behavior_lost' => 'interactive_control',
'html_iframe_embed_fallback' => 'external_embed',
Expand Down
120 changes: 118 additions & 2 deletions php-transformer/src/HtmlToBlocks/HtmlTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ public function transform(string $html, array $options = array()): TransformerRe
$this->recordRuntimeIslandsForPreservedHtmlBlocks($blocks);
$this->appendInteractiveControlBehaviorLossFallbacks($body, $fallbacks);
$this->appendProductGridFallbacks($body, $fallbacks);
$this->appendCommerceControlsFallbacks($body, $fallbacks);
$sourceProvenance = $this->sourceProvenanceForBlocks($blocks);
$serializedBlocks = $this->runtime->serializeBlocks($blocks);
$blockValidityReport = $this->runtime->validateBlockSerialization($blocks);
Expand Down Expand Up @@ -3473,7 +3474,7 @@ private function appendProductGridFallbacks(DOMElement $body, array &$fallbacks)
'reason' => 'commerce_product_grid_detected',
'diagnostic_code' => 'html_product_grid_fallback',
'kind' => 'html_product_grid_fallback',
'message' => 'A product grid was detected; per-card commerce structure was extracted so a consumer can materialize the products.',
'message' => 'A product grid was detected; per-card commerce structure was extracted so a shop provider can materialize the products.',
'source_format' => 'html',
'tag' => strtolower($element->tagName),
'selector' => $this->elementSelector($element),
Expand All @@ -3486,6 +3487,52 @@ private function appendProductGridFallbacks(DOMElement $body, array &$fallbacks)
}
}

/**
* Surface commerce-specific runtime controls separately from the surrounding
* product-grid structure. The transformer can emit editable layout/product
* metadata, but quantity and add-to-cart controls require a commerce runtime.
*
* @param array<int, array<string, mixed>> $fallbacks
*/
private function appendCommerceControlsFallbacks(DOMElement $body, array &$fallbacks): void
{
$emitted = 0;
foreach ( $body->getElementsByTagName('*') as $element ) {
if ( ! $element instanceof DOMElement ) {
continue;
}

if ( $emitted >= self::MAX_INTERACTION_CANDIDATES ) {
return;
}

if ( ! $this->isProductGridContainer($element) ) {
continue;
}

$controlGroups = $this->commerceControlGroupsForContainer($element);
if ( array() === $controlGroups ) {
continue;
}

$fallbacks[] = FallbackDiagnostic::build(array_filter(array(
'type' => 'html',
'reason' => 'commerce_controls_require_runtime',
'diagnostic_code' => 'html_commerce_controls_fallback',
'kind' => 'html_commerce_controls_fallback',
'message' => 'Commerce quantity and add-to-cart controls were detected; product data can be seeded by a shop provider, but these controls need cart runtime binding rather than a static core block approximation.',
'source_format' => 'html',
'tag' => strtolower($element->tagName),
'selector' => $this->elementSelector($element),
'container_selector' => $this->elementSelector($element),
'context' => $this->sourceContext($element),
'controls' => $controlGroups,
'control_count' => count($controlGroups),
), static fn (mixed $value): bool => null !== $value && '' !== $value && array() !== $value), $this->fallbackProvenance);
++$emitted;
}
}

/**
* Whether an element is a plausible product-grid container: a list (ul/ol) or
* an element the structure classifier already flags as grid_like.
Expand Down Expand Up @@ -3525,6 +3572,35 @@ private function productCardsForContainer(DOMElement $container): array
return $products;
}

/**
* @return array<int, array<string, mixed>>
*/
private function commerceControlGroupsForContainer(DOMElement $container): array
{
$groups = array();
foreach ( $container->childNodes as $child ) {
if ( ! $child instanceof DOMElement ) {
continue;
}

$product = $this->productCardData($child);
if ( null === $product || empty($product['has_cart_control']) ) {
continue;
}

$hasQuantity = $this->hasQuantityControl($child);
$groups[] = array_filter(array(
'product_name' => $product['name'] ?? '',
'source_selector' => $this->elementSelector($child),
'has_quantity_control' => $hasQuantity,
'has_cart_control' => true,
'runtime_requirement' => 'commerce_cart_runtime',
), static fn (mixed $value): bool => null !== $value && '' !== $value);
}

return $groups;
}

/**
* Build the per-card product payload when a card qualifies, else null.
*
Expand Down Expand Up @@ -3765,6 +3841,46 @@ private function hasCartControl(DOMElement $card): bool
return false;
}

/**
* Whether the card contains quantity UI: number input, spinbutton, +/- controls,
* or explicit quantity labels/classes/ARIA. This is diagnostic only.
*/
private function hasQuantityControl(DOMElement $card): bool
{
foreach ( $card->getElementsByTagName('*') as $descendant ) {
if ( ! $descendant instanceof DOMElement ) {
continue;
}

$tagName = strtolower($descendant->tagName);
$role = strtolower($this->attr($descendant, 'role'));
if ( 'input' === $tagName && 'number' === strtolower($this->attr($descendant, 'type')) ) {
return true;
}
if ( 'spinbutton' === $role ) {
return true;
}

$haystack = strtolower(implode(' ', array(
$this->attr($descendant, 'class'),
$this->attr($descendant, 'id'),
$this->attr($descendant, 'name'),
$this->attr($descendant, 'aria-label'),
implode(' ', $this->safeDataAttributes($descendant)),
$this->collapsedText($descendant),
)));

if ( preg_match('/\b(?:qty|quantity|decrease|increase)\b/', $haystack) ) {
return true;
}
if ( in_array($tagName, array( 'button', 'a' ), true) && preg_match('/^[+\x{2212}-]$/u', trim($this->collapsedText($descendant))) ) {
return true;
}
}

return false;
}

/**
* An optional short product description: the first paragraph in the card whose
* text is neither the name nor a price. Returns null when none is present.
Expand Down Expand Up @@ -5546,7 +5662,7 @@ private function formFallbackFinding(DOMElement $element, ?array $readableFormBl
'type' => 'html',
'reason' => 'form_requires_runtime',
'diagnostic_code' => 'html_form_fallback',
'message' => 'Form HTML requires runtime behavior and was preserved as safe fallback metadata.',
'message' => 'Form intent and controls were extracted as provider-materializable metadata; the source form markup is preserved until a form provider materializes it.',
'source_format' => 'html',
'tag' => strtolower($element->tagName),
'selector' => $this->elementSelector($element),
Expand Down
Loading
Loading