diff --git a/composer.lock b/composer.lock index f45422f..b0751d5 100644 --- a/composer.lock +++ b/composer.lock @@ -726,12 +726,12 @@ "source": { "type": "git", "url": "https://github.com/chubes4/html-to-blocks-converter.git", - "reference": "a00e54228913474d2788802ea95d778c53a31ee7" + "reference": "5afe78315a77ccae5ddef74e5967e476aa7a615d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/chubes4/html-to-blocks-converter/zipball/a00e54228913474d2788802ea95d778c53a31ee7", - "reference": "a00e54228913474d2788802ea95d778c53a31ee7", + "url": "https://api.github.com/repos/chubes4/html-to-blocks-converter/zipball/5afe78315a77ccae5ddef74e5967e476aa7a615d", + "reference": "5afe78315a77ccae5ddef74e5967e476aa7a615d", "shasum": "" }, "require": { @@ -759,7 +759,7 @@ "source": "https://github.com/chubes4/html-to-blocks-converter/tree/main", "issues": "https://github.com/chubes4/html-to-blocks-converter/issues" }, - "time": "2026-05-31T19:31:07+00:00" + "time": "2026-06-07T18:18:54+00:00" }, { "name": "fidry/console", diff --git a/vendor_prefixed/chubes4/html-to-blocks-converter/html-to-blocks-converter.php b/vendor_prefixed/chubes4/html-to-blocks-converter/html-to-blocks-converter.php index c0d8643..9b049fe 100644 --- a/vendor_prefixed/chubes4/html-to-blocks-converter/html-to-blocks-converter.php +++ b/vendor_prefixed/chubes4/html-to-blocks-converter/html-to-blocks-converter.php @@ -6,7 +6,7 @@ * Plugin Name: HTML to Blocks Converter * Plugin URI: https://github.com/chubes4/html-to-blocks-converter * Description: Converts raw HTML to Gutenberg blocks — on write (wp_insert_post) and on read (REST API for the editor) - * Version: 0.7.1 + * Version: 0.7.2 * Author: Chris Huber * License: GPL v2 or later * License URI: https://www.gnu.org/licenses/gpl-2.0.html diff --git a/vendor_prefixed/chubes4/html-to-blocks-converter/includes/class-svg-icon-classifier.php b/vendor_prefixed/chubes4/html-to-blocks-converter/includes/class-svg-icon-classifier.php index a0e6d0e..f4946cb 100644 --- a/vendor_prefixed/chubes4/html-to-blocks-converter/includes/class-svg-icon-classifier.php +++ b/vendor_prefixed/chubes4/html-to-blocks-converter/includes/class-svg-icon-classifier.php @@ -15,9 +15,9 @@ class HTML_To_Blocks_SVG_Icon_Classifier private const MAX_BYTES = 5120; private const MAX_NODES = 50; private const MAX_DEPTH = 5; - private const MAX_ICON_SIZE = 256; - private const ALLOWED_TAGS = array('svg', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon', 'ellipse', 'g', 'title', 'desc'); - private const ALLOWED_ATTRIBUTES = array('aria-hidden', 'aria-label', 'class', 'cx', 'cy', 'd', 'fill', 'fill-opacity', 'height', 'points', 'r', 'role', 'rx', 'ry', 'stroke', 'stroke-linecap', 'stroke-linejoin', 'stroke-opacity', 'stroke-width', 'viewbox', 'width', 'x', 'x1', 'x2', 'y', 'y1', 'y2'); + private const MAX_GRAPHIC_SIZE = 512; + private const ALLOWED_TAGS = array('svg', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon', 'ellipse', 'g', 'defs', 'pattern', 'title', 'desc'); + private const ALLOWED_ATTRIBUTES = array('aria-hidden', 'aria-label', 'aria-labelledby', 'class', 'cx', 'cy', 'd', 'fill', 'fill-opacity', 'height', 'id', 'patternunits', 'points', 'r', 'role', 'rx', 'ry', 'stroke', 'stroke-linecap', 'stroke-linejoin', 'stroke-opacity', 'stroke-width', 'viewbox', 'width', 'x', 'x1', 'x2', 'y', 'y1', 'y2'); /** * Classifies a source fragment as a safe inline SVG icon or a rejected SVG. * @@ -45,7 +45,7 @@ public static function classify(string $svg): array $result['reason'] = 'invalid_svg'; return $result; } - $state = array('nodes' => 0, 'max_depth' => 0, 'tags' => array(), 'reason' => ''); + $state = array('nodes' => 0, 'max_depth' => 0, 'tags' => array(), 'reason' => '', 'local_refs' => self::collect_local_reference_ids($document->documentElement)); $sanitized = self::sanitize_element($document->documentElement, $document, 1, $state); if (!$sanitized) { $result['reason'] = $state['reason'] ? $state['reason'] : 'unsafe_svg'; @@ -54,7 +54,7 @@ public static function classify(string $svg): array $view_box = $sanitized->getAttribute('viewBox'); $width = $sanitized->getAttribute('width'); $height = $sanitized->getAttribute('height'); - if (!self::has_small_icon_dimensions($view_box, $width, $height)) { + if (!self::has_bounded_graphic_dimensions($view_box, $width, $height)) { $result['reason'] = 'dimension_limit'; return $result; } @@ -63,10 +63,11 @@ public static function classify(string $svg): array $result['reason'] = 'serialization_failed'; return $result; } + $is_icon_sized = self::is_icon_sized_graphic($view_box, $width, $height); $result['is_safe'] = \true; $result['svg'] = $sanitized_svg; - $result['reason'] = 'safe_svg_icon'; - $result['metadata'] = array('kind' => 'inline-svg-icon', 'viewBox' => $view_box, 'width' => $width, 'height' => $height, 'className' => $sanitized->getAttribute('class'), 'ariaLabel' => $sanitized->getAttribute('aria-label'), 'nodeCount' => $state['nodes'], 'maxDepth' => $state['max_depth'], 'tags' => \array_values(\array_unique($state['tags']))); + $result['reason'] = $is_icon_sized ? 'safe_svg_icon' : 'safe_inline_svg_illustration'; + $result['metadata'] = array('kind' => $is_icon_sized ? 'inline-svg-icon' : 'inline-svg-illustration', 'viewBox' => $view_box, 'width' => $width, 'height' => $height, 'className' => $sanitized->getAttribute('class'), 'ariaLabel' => $sanitized->getAttribute('aria-label'), 'nodeCount' => $state['nodes'], 'maxDepth' => $state['max_depth'], 'tags' => \array_values(\array_unique($state['tags']))); return $result; } /** @@ -96,7 +97,7 @@ private static function sanitize_element(\DOMElement $source, \DOMDocument $docu foreach (\iterator_to_array($source->attributes) as $attribute) { $name = \strtolower($attribute->name); $value = \trim($attribute->value); - if (!self::is_allowed_attribute($name, $value)) { + if (!self::is_allowed_attribute($name, $value, $state)) { $state['reason'] = 'disallowed_attribute'; return null; } @@ -132,28 +133,56 @@ private static function sanitize_element(\DOMElement $source, \DOMDocument $docu * @param string $value Attribute value. * @return bool True when safe. */ - private static function is_allowed_attribute(string $name, string $value): bool + private static function is_allowed_attribute(string $name, string $value, array $state): bool { if (\strpos($name, 'on') === 0 || \in_array($name, array('href', 'xlink:href', 'src', 'style'), \true)) { return \false; } + if ('id' === $name) { + return \preg_match('/^[A-Za-z][A-Za-z0-9_-]*$/', $value) === 1; + } if ('xmlns' === $name && 'http://www.w3.org/2000/svg' === $value) { return \true; } if (!\in_array($name, self::ALLOWED_ATTRIBUTES, \true)) { return \false; } - return \preg_match('/url\s*\(|(?:https?:)?\/\/|data:/i', $value) !== 1; + if (\preg_match('/url\s*\(/i', $value) === 1) { + if (\preg_match('/^url\(#([A-Za-z][A-Za-z0-9_-]*)\)$/', $value, $matches) !== 1) { + return \false; + } + return \in_array($matches[1], $state['local_refs'] ?? array(), \true); + } + return \preg_match('/(?:https?:)?\/\/|data:/i', $value) !== 1; + } + /** + * Collects local paint server IDs that are safe to reference via url(#id). + * + * @param DOMElement $root Root SVG element. + * @return string[] Local reference IDs. + */ + private static function collect_local_reference_ids(\DOMElement $root): array + { + $ids = array(); + foreach ($root->getElementsByTagName('pattern') as $pattern) { + if ($pattern instanceof \DOMElement && $pattern->hasAttribute('id')) { + $id = \trim($pattern->getAttribute('id')); + if (\preg_match('/^[A-Za-z][A-Za-z0-9_-]*$/', $id) === 1) { + $ids[] = $id; + } + } + } + return \array_values(\array_unique($ids)); } /** - * Applies small icon dimension limits. + * Applies bounded inline graphic dimension limits. * * @param string $view_box SVG viewBox attribute. * @param string $width SVG width attribute. * @param string $height SVG height attribute. - * @return bool True when dimensions look icon-sized. + * @return bool True when dimensions look bounded. */ - private static function has_small_icon_dimensions(string $view_box, string $width, string $height): bool + private static function has_bounded_graphic_dimensions(string $view_box, string $width, string $height): bool { if ('' !== $view_box) { $parts = \preg_split('/[\s,]+/', \trim($view_box)); @@ -163,7 +192,7 @@ private static function has_small_icon_dimensions(string $view_box, string $widt if (\count($parts) !== 4 || !\is_numeric($parts[2]) || !\is_numeric($parts[3])) { return \false; } - if ((float) $parts[2] <= 0 || (float) $parts[3] <= 0 || (float) $parts[2] > self::MAX_ICON_SIZE || (float) $parts[3] > self::MAX_ICON_SIZE) { + if ((float) $parts[2] <= 0 || (float) $parts[3] <= 0 || (float) $parts[2] > self::MAX_GRAPHIC_SIZE || (float) $parts[3] > self::MAX_GRAPHIC_SIZE) { return \false; } } @@ -171,7 +200,31 @@ private static function has_small_icon_dimensions(string $view_box, string $widt if ('' === $dimension) { continue; } - if (\preg_match('/^([0-9]+(?:\.[0-9]+)?)(?:px)?$/', $dimension, $matches) !== 1 || (float) $matches[1] > self::MAX_ICON_SIZE) { + if (\preg_match('/^([0-9]+(?:\.[0-9]+)?)(?:px)?$/', $dimension, $matches) !== 1 || (float) $matches[1] > self::MAX_GRAPHIC_SIZE) { + return \false; + } + } + return '' !== $view_box || '' !== $width || '' !== $height; + } + /** + * Checks whether a bounded SVG is small enough to keep icon metadata. + * + * @param string $view_box SVG viewBox attribute. + * @param string $width SVG width attribute. + * @param string $height SVG height attribute. + * @return bool True when dimensions look icon-sized. + */ + private static function is_icon_sized_graphic(string $view_box, string $width, string $height): bool + { + if ('' !== $view_box) { + $parts = \preg_split('/[\s,]+/', \trim($view_box)); + return \is_array($parts) && \count($parts) === 4 && \is_numeric($parts[2]) && \is_numeric($parts[3]) && (float) $parts[2] <= 256 && (float) $parts[3] <= 256; + } + foreach (array($width, $height) as $dimension) { + if ('' === $dimension) { + continue; + } + if (\preg_match('/^([0-9]+(?:\.[0-9]+)?)(?:px)?$/', $dimension, $matches) !== 1 || (float) $matches[1] > 256) { return \false; } } diff --git a/vendor_prefixed/chubes4/html-to-blocks-converter/includes/class-transform-registry.php b/vendor_prefixed/chubes4/html-to-blocks-converter/includes/class-transform-registry.php index 1d075cc..bfb6de9 100644 --- a/vendor_prefixed/chubes4/html-to-blocks-converter/includes/class-transform-registry.php +++ b/vendor_prefixed/chubes4/html-to-blocks-converter/includes/class-transform-registry.php @@ -499,13 +499,17 @@ private static function get_list_transforms() })); } /** - * Creates an unordered list block from a simple definition list. + * Creates blocks from a simple definition list. * * @param HTML_To_Blocks_HTML_Element $definition_list The dl element. * @return array Block array. */ private static function create_definition_list_block_from_element($definition_list): array { + $wrapper_pairs = self::get_definition_list_wrapper_pairs($definition_list); + if (!empty($wrapper_pairs)) { + return self::create_visual_definition_list_group_from_pairs($definition_list, $wrapper_pairs); + } $list_attributes = self::get_block_support_attributes($definition_list, array('anchor' => \true, 'class_name' => \true, 'colors' => \true, 'spacing' => \true, 'border' => \true)); $list_attributes = \array_merge($list_attributes, array('ordered' => \false)); $inner_blocks = array(); @@ -514,6 +518,44 @@ private static function create_definition_list_block_from_element($definition_li } return HTML_To_Blocks_Block_Factory::create_block('core/list', $list_attributes, $inner_blocks); } + /** + * Creates grouped native blocks for generated visual metadata definition lists. + * + * @param HTML_To_Blocks_HTML_Element $definition_list The dl element. + * @param array $wrapper_pairs + * Definition wrapper pairs. + * @return array Block array. + */ + private static function create_visual_definition_list_group_from_pairs($definition_list, array $wrapper_pairs): array + { + $inner_blocks = array(); + foreach ($wrapper_pairs as $pair) { + $inner_blocks[] = HTML_To_Blocks_Block_Factory::create_block('core/group', self::get_visual_definition_list_group_attributes($pair['wrapper']), array(self::create_definition_list_paragraph_block($pair['term']), self::create_definition_list_paragraph_block($pair['description']))); + } + return HTML_To_Blocks_Block_Factory::create_block('core/group', self::get_visual_definition_list_group_attributes($definition_list), $inner_blocks); + } + /** + * Creates a paragraph block for a definition term or description. + * + * @param HTML_To_Blocks_HTML_Element $element Source dt or dd element. + * @return array Block array. + */ + private static function create_definition_list_paragraph_block($element): array + { + $attributes = self::get_block_support_attributes($element, array('class_name' => \true, 'colors' => \true, 'typography' => \true, 'spacing' => \true, 'text_align' => \true)); + $attributes['content'] = \trim($element->get_inner_html()); + return HTML_To_Blocks_Block_Factory::create_block('core/paragraph', $attributes); + } + /** + * Gets wrapper-safe attributes for visual definition-list groups. + * + * @param HTML_To_Blocks_HTML_Element $element Source dl or wrapper element. + * @return array Block attributes. + */ + private static function get_visual_definition_list_group_attributes($element): array + { + return self::get_block_support_attributes($element, array('anchor' => \true, 'class_name' => \true, 'align' => \true, 'colors' => \true, 'spacing' => \true, 'border' => \true, 'dimensions' => \true, 'layout' => \true, 'aria_label' => \true)); + } /** * Gets simple term/description pairs from a dl element. * @@ -530,7 +572,7 @@ private static function get_definition_list_pairs($definition_list): array if (empty($children)) { return array(); } - if (self::definition_list_has_only_wrapper_pairs($children)) { + if (!empty(self::get_definition_list_wrapper_pairs($definition_list))) { $pairs = array(); foreach ($children as $child) { $pairs[] = self::get_definition_pair_from_wrapper($child); @@ -539,6 +581,26 @@ private static function get_definition_list_pairs($definition_list): array } return self::get_direct_definition_list_pairs($children); } + /** + * Gets simple wrapper pairs from generated dl > div > dt + dd markup. + * + * @param HTML_To_Blocks_HTML_Element $definition_list The dl element. + * @return array Wrapper + * pair data. + */ + private static function get_definition_list_wrapper_pairs($definition_list): array + { + $children = $definition_list->get_child_elements(); + if (empty($children) || !self::definition_list_has_only_wrapper_pairs($children)) { + return array(); + } + $pairs = array(); + foreach ($children as $child) { + $wrapper_children = $child->get_child_elements(); + $pairs[] = array('wrapper' => $child, 'term' => $wrapper_children[0], 'description' => $wrapper_children[1]); + } + return $pairs; + } /** * Checks whether all direct dl children are div wrappers around dt/dd pairs. * @@ -814,6 +876,10 @@ private static function get_button_transforms() return self::is_static_visual_button_container($element); }, 'transform' => function ($element) { return self::create_static_visual_button_group($element); + }), array('blockName' => 'core/paragraph', 'priority' => 8, 'selector' => 'button', 'isMatch' => function ($element) { + return self::is_static_navigation_toggle_button($element); + }, 'transform' => function ($element) { + return self::create_static_navigation_toggle_paragraph($element); }), array('blockName' => 'core/paragraph', 'priority' => 8, 'selector' => 'button', 'isMatch' => function ($element) { return self::is_static_visual_button($element); }, 'transform' => function ($element) { @@ -842,6 +908,41 @@ private static function get_button_transforms() return self::create_buttons_block_from_anchor($anchor); })); } + /** + * Checks whether a button is static responsive-navigation chrome. + * + * @param HTML_To_Blocks_HTML_Element $element Element to inspect. + * @return bool True when the button can be represented as native editable text. + */ + private static function is_static_navigation_toggle_button($element): bool + { + if ('BUTTON' !== $element->get_tag_name() || !$element->has_attribute('class')) { + return \false; + } + if (!self::class_matches($element, '/(?:^|[-_\s])(?:nav|menu)[-_\s]?toggle(?:$|[-_\s])/i')) { + return \false; + } + if ($element->has_attribute('form') || $element->has_attribute('name') || $element->has_attribute('value')) { + return \false; + } + $type = \strtolower(\trim((string) ($element->get_attribute('type') ?? ''))); + if (\in_array($type, array('submit', 'reset'), \true)) { + return \false; + } + return '' !== \trim($element->get_text_content()); + } + /** + * Creates editable text for static responsive-navigation chrome. + * + * @param HTML_To_Blocks_HTML_Element $element Button element. + * @return array Block array. + */ + private static function create_static_navigation_toggle_paragraph($element): array + { + $attributes = self::get_block_support_attributes($element, array('anchor' => \true, 'class_name' => \true)); + $attributes['content'] = esc_html(\trim($element->get_text_content())); + return HTML_To_Blocks_Block_Factory::create_block('core/paragraph', $attributes); + } /** * Checks whether an element is a simple row/container of button-like anchors. * @@ -1134,7 +1235,7 @@ private static function is_branded_inline_anchor($element): bool return \false; } $href = \trim((string) ($element->get_attribute('href') ?? '')); - if ('' === $href || '#' !== $href[0]) { + if ('' === $href || \preg_match('/^(?:https?:|mailto:|tel:|javascript:)/i', $href) === 1) { return \false; } if (!self::class_matches($element, '/(?:^|[-_\s])brand(?:$|[-_\s])/i')) { @@ -1546,7 +1647,11 @@ private static function get_details_transforms() $summary = \trim($summary_matches[1] ?? ''); $content_html = \trim(\preg_replace('/]*)?>.*?<\/summary>/is', '', $inner_html, 1)); $inner_blocks = '' !== $content_html ? $handler(array('HTML' => $content_html)) : array(); - $attributes = array('summary' => $summary); + $attributes = self::get_block_support_attributes($element, array('anchor' => \true, 'class_name' => \true)); + $attributes['summary'] = $summary; + if ($element->has_attribute('open')) { + $attributes['showContent'] = \true; + } return HTML_To_Blocks_Block_Factory::create_block('core/details', $attributes, $inner_blocks); })); } @@ -2240,13 +2345,24 @@ private static function is_static_placeholder_form($element): bool if ('FORM' !== $element->get_tag_name()) { return \false; } - if (!$element->has_attribute('action')) { + if (!self::has_direct_form_controls($element)) { return \false; } - $action = \trim((string) $element->get_attribute('action')); - if ('#' !== $action) { - return \false; + $action = $element->has_attribute('action') ? \trim((string) $element->get_attribute('action')) : ''; + $method = $element->has_attribute('method') ? \trim((string) $element->get_attribute('method')) : ''; + if ('#' === $action) { + return \true; } + return '' === $action && '' === $method && self::has_static_placeholder_form_marker($element); + } + /** + * Checks whether a form has direct controls worth preserving. + * + * @param HTML_To_Blocks_HTML_Element $element Source form element. + * @return bool True when direct form controls are present. + */ + private static function has_direct_form_controls($element): bool + { foreach ($element->get_child_elements() as $child) { if (\in_array($child->get_tag_name(), array('LABEL', 'INPUT', 'TEXTAREA', 'SELECT', 'BUTTON'), \true)) { return \true; @@ -2254,6 +2370,20 @@ private static function is_static_placeholder_form($element): bool } return \false; } + /** + * Checks whether a no-target form is explicitly presented as static preview copy. + * + * @param HTML_To_Blocks_HTML_Element $element Source form element. + * @return bool True when the source marks the form as inert preview/checklist UI. + */ + private static function has_static_placeholder_form_marker($element): bool + { + $class = $element->has_attribute('class') ? (string) $element->get_attribute('class') : ''; + $aria_label = $element->has_attribute('aria-label') ? (string) $element->get_attribute('aria-label') : ''; + $text = wp_strip_all_tags($element->get_inner_html()); + $haystack = \strtolower($class . ' ' . $aria_label . ' ' . $text); + return \false !== \strpos($haystack, 'static-form') || \false !== \strpos($haystack, 'form-card') || \false !== \strpos($haystack, 'static preview') || \false !== \strpos($haystack, 'preview form') || \false !== \strpos($haystack, 'preview only') || \false !== \strpos($haystack, 'checklist'); + } /** * Creates a native group from a static placeholder form. * @@ -2261,15 +2391,31 @@ private static function is_static_placeholder_form($element): bool * @return array Block array. */ private static function create_static_placeholder_form_group($element): array + { + $inner_blocks = self::create_static_placeholder_form_child_blocks($element->get_child_elements()); + return HTML_To_Blocks_Block_Factory::create_block('core/group', self::get_common_layout_attributes($element), $inner_blocks); + } + /** + * Creates editable blocks for static placeholder form children. + * + * @param array $children Child elements. + * @return array> Block arrays. + */ + private static function create_static_placeholder_form_child_blocks(array $children): array { $inner_blocks = array(); - foreach ($element->get_child_elements() as $child) { + foreach ($children as $child) { $tag = $child->get_tag_name(); + if (\preg_match('/^H([1-6])$/', $tag, $matches)) { + $inner_blocks[] = HTML_To_Blocks_Block_Factory::create_block('core/heading', array('level' => (int) $matches[1], 'content' => \trim($child->get_inner_html()))); + continue; + } if ('LABEL' === $tag) { $content = self::get_static_form_label_text($child); if ('' !== $content) { $inner_blocks[] = HTML_To_Blocks_Block_Factory::create_block('core/paragraph', array('content' => esc_html($content))); } + $inner_blocks = \array_merge($inner_blocks, self::create_static_placeholder_form_child_blocks($child->get_child_elements())); continue; } if ('BUTTON' === $tag) { @@ -2279,14 +2425,58 @@ private static function create_static_placeholder_form_group($element): array } continue; } + if ('P' === $tag) { + $content = \trim(wp_strip_all_tags($child->get_inner_html())); + if ('' !== $content) { + $attributes = self::get_block_support_attributes($child, array('class_name' => \true)); + $attributes['content'] = esc_html(\preg_replace('/\s+/', ' ', $content)); + $inner_blocks[] = HTML_To_Blocks_Block_Factory::create_block('core/paragraph', $attributes); + } + continue; + } + if ('SELECT' === $tag) { + $option_blocks = self::create_static_form_option_blocks($child); + if (!empty($option_blocks)) { + $inner_blocks[] = HTML_To_Blocks_Block_Factory::create_block('core/list', array(), $option_blocks); + continue; + } + } if (\in_array($tag, array('INPUT', 'TEXTAREA', 'SELECT'), \true)) { $content = self::get_static_form_control_label($child); if ('' !== $content) { $inner_blocks[] = HTML_To_Blocks_Block_Factory::create_block('core/paragraph', array('content' => esc_html($content))); } + continue; + } + if (\in_array($tag, array('DIV', 'SECTION'), \true)) { + $child_blocks = self::create_static_placeholder_form_child_blocks($child->get_child_elements()); + if (!empty($child_blocks)) { + $inner_blocks[] = HTML_To_Blocks_Block_Factory::create_block('core/group', self::get_common_layout_attributes($child), $child_blocks); + } } } - return HTML_To_Blocks_Block_Factory::create_block('core/group', self::get_common_layout_attributes($element), $inner_blocks); + return $inner_blocks; + } + /** + * Creates editable list item blocks from visible static select options. + * + * @param HTML_To_Blocks_HTML_Element $select Select element. + * @return array> Option list-item blocks. + */ + private static function create_static_form_option_blocks($select): array + { + $option_blocks = array(); + foreach ($select->get_child_elements() as $child) { + if ('OPTION' !== $child->get_tag_name()) { + continue; + } + $content = \trim(wp_strip_all_tags($child->get_inner_html())); + if ('' === $content) { + continue; + } + $option_blocks[] = HTML_To_Blocks_Block_Factory::create_block('core/list-item', array('content' => esc_html(\preg_replace('/\s+/', ' ', $content)))); + } + return $option_blocks; } /** * Gets visible text for a static form label. @@ -2421,7 +2611,7 @@ private static function get_layout_transforms() */ private static function get_empty_decorative_group_attributes($element): array { - return self::get_block_support_attributes($element, array('anchor' => \true, 'class_name' => \true, 'align' => \true, 'colors' => \true, 'dimensions' => \true, 'spacing' => \true, 'border' => \true)); + return self::get_block_support_attributes($element, array('anchor' => \true, 'class_name' => \true, 'align' => \true, 'colors' => \true, 'dimensions' => \true, 'spacing' => \true, 'border' => \true, 'aria_label' => \true)); } /** * Gets attributes shared by layout blocks. @@ -3092,7 +3282,7 @@ private static function create_card_grid_item_inner_blocks($element, ?array $chi $image_src = $image->get_attribute('src') ?? ''; $has_image = \false; foreach ($blocks as $block) { - if ('core/image' === ($block['blockName'] ?? '') && $image_src === ($block['attrs']['url'] ?? '')) { + if ('core/image' === ($block['blockName'] ?? '') && ($block['attrs']['url'] ?? '') === $image_src) { $has_image = \true; break; } @@ -3324,7 +3514,7 @@ private static function create_inline_scroller_child_blocks($element, callable $ */ private static function is_empty_decorative_element($element): bool { - return self::is_empty_element($element) && (self::is_project_card_status_element($element) || self::class_matches($element, '/(?:^|[-_\s])(background|bg|pattern|texture|divider|separator|connector|rule|ruler|line|blank|overlay|grain|noise|glow|gradient|scan|dot|mark|bullet|icon|orb|blob|fill|img|image|media|photo|picture|thumb|progress|meter|gauge|today|traffic[-_]?light|tl[-_]?(?:red|yellow|green)|task[-_\s]?check)(?:$|[-_\s]|\d)/i') || self::has_visual_placeholder_background($element)); + return self::is_empty_element($element) && (self::is_project_card_status_element($element) || self::class_matches($element, '/(?:^|[-_\s])(background|bg|pattern|texture|divider|separator|connector|rule|ruler|line|blank|overlay|grain|noise|glow|gradient|scan|dot|mark|bullet|icon|orb|blob|fill|img|image|media|photo|picture|thumb|patch|progress|meter|gauge|today|traffic[-_]?light|tl[-_]?(?:red|yellow|green)|task[-_\s]?check)(?:$|[-_\s]|\d)/i') || self::has_visual_placeholder_background($element)); } /** * Checks whether a figure wraps a decorative visual placeholder and caption. @@ -3361,11 +3551,31 @@ private static function is_nested_empty_decorative_element($element): bool if (\trim(wp_strip_all_tags($element->get_inner_html())) !== '') { return \false; } - foreach ($element->query_selector_all('a, button, input, select, textarea, img, video, audio, iframe, object, embed, svg') as $functional_child) { - return \false; + foreach ($element->get_child_elements() as $child) { + if (self::is_functional_element_or_descendant($child)) { + return \false; + } } return !empty($element->get_child_elements()); } + /** + * Checks whether an element subtree contains controls, media, or embeds. + * + * @param HTML_To_Blocks_HTML_Element $element Source element. + * @return bool True when the subtree contains functional markup. + */ + private static function is_functional_element_or_descendant($element): bool + { + if (\in_array($element->get_tag_name(), array('A', 'BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'IMG', 'VIDEO', 'AUDIO', 'IFRAME', 'OBJECT', 'EMBED', 'SVG'), \true)) { + return \true; + } + foreach ($element->get_child_elements() as $child) { + if (self::is_functional_element_or_descendant($child)) { + return \true; + } + } + return \false; + } /** * Checks whether an empty element carries visual background styling. * @@ -3521,7 +3731,7 @@ private static function extract_background_color(string $style): string */ private static function get_paragraph_transforms() { - return array(array('blockName' => 'core/paragraph', 'priority' => 20, 'selector' => 'p,address,a,label,div,span', 'isMatch' => function ($element) { + return array(array('blockName' => 'core/paragraph', 'priority' => 20, 'selector' => 'p,address,a,label,div,span,strong,em', 'isMatch' => function ($element) { if (\in_array($element->get_tag_name(), array('P', 'ADDRESS', 'A'), \true)) { return \true; } @@ -3534,12 +3744,18 @@ private static function get_paragraph_transforms() if ('SPAN' === $element->get_tag_name() && $element->has_attribute('class')) { return \false; } + if (\in_array($element->get_tag_name(), array('STRONG', 'EM'), \true)) { + return array() === $element->get_child_elements() && \trim($element->get_text_content()) !== ''; + } return \in_array($element->get_tag_name(), array('DIV', 'SPAN'), \true) && array() === $element->get_child_elements() && \trim($element->get_text_content()) !== ''; }, 'transform' => function ($element) { if (self::is_inline_span_label($element)) { return self::create_inline_span_label_paragraph($element); } $content = $element->get_tag_name() === 'A' ? self::get_paragraph_anchor_content($element) : $element->get_inner_html(); + if (\in_array($element->get_tag_name(), array('STRONG', 'EM'), \true)) { + $content = \trim($element->get_outer_html()); + } if (self::is_static_checkbox_label($element)) { $content = \trim(\preg_replace('/<\s*input\b[^>]*>/i', '', $content)); } diff --git a/vendor_prefixed/chubes4/html-to-blocks-converter/library.php b/vendor_prefixed/chubes4/html-to-blocks-converter/library.php index 82cfe5a..2ea9c26 100644 --- a/vendor_prefixed/chubes4/html-to-blocks-converter/library.php +++ b/vendor_prefixed/chubes4/html-to-blocks-converter/library.php @@ -20,7 +20,7 @@ return; } $html_to_blocks_library_path = __DIR__; -$html_to_blocks_library_version = '0.7.1'; +$html_to_blocks_library_version = '0.7.2'; if (!\class_exists('BlockFormatBridge\Vendor\HTML_To_Blocks_Versions', \false)) { require_once $html_to_blocks_library_path . '/includes/class-html-to-blocks-versions.php'; } diff --git a/vendor_prefixed/chubes4/html-to-blocks-converter/tests/smoke-decorative-div-fallbacks.php b/vendor_prefixed/chubes4/html-to-blocks-converter/tests/smoke-decorative-div-fallbacks.php index 0db86ae..ca69367 100644 --- a/vendor_prefixed/chubes4/html-to-blocks-converter/tests/smoke-decorative-div-fallbacks.php +++ b/vendor_prefixed/chubes4/html-to-blocks-converter/tests/smoke-decorative-div-fallbacks.php @@ -37,7 +37,7 @@ public static function get_instance() } public function is_registered($name) { - return \in_array($name, ['core/group', 'core/html', 'core/paragraph'], \true); + return \in_array($name, ['core/group', 'core/html', 'core/paragraph', 'core/separator'], \true); } public function get_registered($name) { diff --git a/vendor_prefixed/chubes4/html-to-blocks-converter/tests/smoke-decorative-visual-clusters.php b/vendor_prefixed/chubes4/html-to-blocks-converter/tests/smoke-decorative-visual-clusters.php index 7aab8c2..85332f7 100644 --- a/vendor_prefixed/chubes4/html-to-blocks-converter/tests/smoke-decorative-visual-clusters.php +++ b/vendor_prefixed/chubes4/html-to-blocks-converter/tests/smoke-decorative-visual-clusters.php @@ -187,6 +187,21 @@ function serialize_blocks(array $blocks): string $assert(\str_contains($caption_only_figure_serialized, 'gallery-tile tile-script'), 'caption-only-figure-class-survives', $caption_only_figure_serialized); $assert(\str_contains($caption_only_figure_serialized, 'Marked scripts at the table'), 'caption-only-figure-caption-survives', $caption_only_figure_serialized); $assert(!\in_array('core/html', $caption_only_figure_names, \true), 'caption-only-figure-has-no-html-fallback', $caption_only_figure_serialized); +$hearthline_gallery_html = <<<'HTML' + +HTML; +$hearthline_gallery_blocks = html_to_blocks_raw_handler(['HTML' => $hearthline_gallery_html]); +$hearthline_gallery_serialized = serialize_blocks($hearthline_gallery_blocks); +$hearthline_gallery_names = $flatten_block_names($hearthline_gallery_blocks); +$assert(!\in_array('core/html', $hearthline_gallery_names, \true), 'hearthline-gallery-has-no-html-fallback', $hearthline_gallery_serialized); +$assert(\substr_count($hearthline_gallery_serialized, 'photo-card') >= 3, 'hearthline-photo-card-classes-survive', $hearthline_gallery_serialized); +$assert(\str_contains($hearthline_gallery_serialized, 'Corner windows and amber evening light'), 'hearthline-first-caption-survives', $hearthline_gallery_serialized); +$assert(\str_contains($hearthline_gallery_serialized, 'Hands learning a tile-laying game'), 'hearthline-second-caption-survives', $hearthline_gallery_serialized); +$assert(\str_contains($hearthline_gallery_serialized, 'Staff shelf tags by mood and group size'), 'hearthline-third-caption-survives', $hearthline_gallery_serialized); $nested_decorative_figure_html = <<<'HTML' HTML; diff --git a/vendor_prefixed/chubes4/html-to-blocks-converter/tests/smoke-definition-list-transforms.php b/vendor_prefixed/chubes4/html-to-blocks-converter/tests/smoke-definition-list-transforms.php index 234ef08..b9a3d26 100644 --- a/vendor_prefixed/chubes4/html-to-blocks-converter/tests/smoke-definition-list-transforms.php +++ b/vendor_prefixed/chubes4/html-to-blocks-converter/tests/smoke-definition-list-transforms.php @@ -44,7 +44,7 @@ public static function get_instance() } public function is_registered($name) { - return \in_array($name, ['core/html', 'core/list', 'core/list-item'], \true); + return \in_array($name, ['core/heading', 'core/html', 'core/group', 'core/list', 'core/list-item', 'core/paragraph'], \true); } public function get_registered($name) { @@ -98,28 +98,47 @@ function do_action($hook_name, ...$args) return $names; }; $html = <<<'HTML' -
Best seller
Brownie Depth Set
Use case
Glossy tops · dense crumb · deep cocoa
+

Brownie Depth Set

Best seller
Brownie Depth Set
Use case
Glossy tops · dense crumb · deep cocoa
HTML; $blocks = html_to_blocks_raw_handler(['HTML' => $html]); $names = $flatten_block_names($blocks); -$assert(\count($blocks) === 1, 'definition-list-produces-single-block'); -$assert(($blocks[0]['blockName'] ?? '') === 'core/list', 'definition-list-becomes-list'); -$assert(($blocks[0]['attrs']['ordered'] ?? null) === \false, 'definition-list-is-unordered'); -$assert(\count($blocks[0]['innerBlocks'] ?? []) === 2, 'definition-list-keeps-pair-count'); -$assert(!\in_array('core/html', $names, \true), 'definition-list-has-no-core-html', \implode(', ', $names)); -$assert(($blocks[0]['innerBlocks'][0]['attrs']['content'] ?? '') === 'Best seller: Brownie Depth Set', 'definition-list-first-pair-content'); -$assert(($blocks[0]['innerBlocks'][1]['attrs']['content'] ?? '') === 'Use case: Glossy tops · dense crumb · deep cocoa', 'definition-list-second-pair-content'); +$definition_group = null; +foreach ($blocks as $block) { + if (($block['blockName'] ?? '') !== 'core/group') { + continue; + } + foreach ($block['innerBlocks'] ?? [] as $inner_block) { + if (($inner_block['attrs']['className'] ?? '') === 'card-meta') { + $definition_group = $inner_block; + break 2; + } + } +} +$assert(null !== $definition_group, 'visual-definition-list-produces-group'); +$assert(!\in_array('core/html', $names, \true), 'visual-definition-list-has-no-core-html', \implode(', ', $names)); +$assert(!\in_array('core/list', $names, \true), 'visual-definition-list-is-not-bulleted-list', \implode(', ', $names)); +$assert(\count($definition_group['innerBlocks'] ?? []) === 2, 'visual-definition-list-keeps-pair-count'); +$assert(($definition_group['innerBlocks'][0]['attrs']['className'] ?? '') === 'meta-row', 'visual-definition-list-preserves-row-class'); +$assert(($definition_group['innerBlocks'][0]['innerBlocks'][0]['blockName'] ?? '') === 'core/paragraph', 'visual-definition-list-term-is-paragraph'); +$assert(($definition_group['innerBlocks'][0]['innerBlocks'][0]['attrs']['className'] ?? '') === 'meta-label', 'visual-definition-list-preserves-term-class'); +$assert(($definition_group['innerBlocks'][0]['innerBlocks'][0]['attrs']['content'] ?? '') === 'Best seller', 'visual-definition-list-preserves-term-content'); +$assert(($definition_group['innerBlocks'][0]['innerBlocks'][1]['attrs']['className'] ?? '') === 'meta-value', 'visual-definition-list-preserves-description-class'); +$assert(($definition_group['innerBlocks'][0]['innerBlocks'][1]['attrs']['content'] ?? '') === 'Brownie Depth Set', 'visual-definition-list-preserves-description-content'); $direct_blocks = html_to_blocks_raw_handler(['HTML' => '
Origin
Charleston
']); $assert(($direct_blocks[0]['blockName'] ?? '') === 'core/list', 'direct-definition-list-becomes-list'); $assert(($direct_blocks[0]['innerBlocks'][0]['attrs']['content'] ?? '') === 'Origin: Charleston', 'direct-definition-list-content'); $wrapper_stat_blocks = html_to_blocks_raw_handler(['HTML' => '
5
workflow categories
18+
bench-ready tools
0
guesswork mornings
']); $assert(\count($wrapper_stat_blocks) === 1, 'wrapped-stat-definition-list-produces-single-block'); -$assert(($wrapper_stat_blocks[0]['blockName'] ?? '') === 'core/list', 'wrapped-stat-definition-list-becomes-list'); +$assert(($wrapper_stat_blocks[0]['blockName'] ?? '') === 'core/group', 'wrapped-stat-definition-list-becomes-group'); $assert(($wrapper_stat_blocks[0]['attrs']['className'] ?? '') === 'hero-stats', 'wrapped-stat-definition-list-preserves-class'); $assert(\count($wrapper_stat_blocks[0]['innerBlocks'] ?? []) === 3, 'wrapped-stat-definition-list-keeps-pair-count'); -$assert(($wrapper_stat_blocks[0]['innerBlocks'][0]['attrs']['content'] ?? '') === '5: workflow categories', 'wrapped-stat-definition-list-first-content'); -$assert(($wrapper_stat_blocks[0]['innerBlocks'][1]['attrs']['content'] ?? '') === '18+: bench-ready tools', 'wrapped-stat-definition-list-second-content'); -$assert(($wrapper_stat_blocks[0]['innerBlocks'][2]['attrs']['content'] ?? '') === '0: guesswork mornings', 'wrapped-stat-definition-list-third-content'); +$assert(($wrapper_stat_blocks[0]['attrs']['ariaLabel'] ?? '') === 'Store highlights', 'wrapped-stat-definition-list-preserves-aria-label'); +$assert(($wrapper_stat_blocks[0]['innerBlocks'][0]['innerBlocks'][0]['attrs']['content'] ?? '') === '5', 'wrapped-stat-definition-list-first-term'); +$assert(($wrapper_stat_blocks[0]['innerBlocks'][0]['innerBlocks'][1]['attrs']['content'] ?? '') === 'workflow categories', 'wrapped-stat-definition-list-first-description'); +$assert(($wrapper_stat_blocks[0]['innerBlocks'][1]['innerBlocks'][0]['attrs']['content'] ?? '') === '18+', 'wrapped-stat-definition-list-second-term'); +$assert(($wrapper_stat_blocks[0]['innerBlocks'][1]['innerBlocks'][1]['attrs']['content'] ?? '') === 'bench-ready tools', 'wrapped-stat-definition-list-second-description'); +$assert(($wrapper_stat_blocks[0]['innerBlocks'][2]['innerBlocks'][0]['attrs']['content'] ?? '') === '0', 'wrapped-stat-definition-list-third-term'); +$assert(($wrapper_stat_blocks[0]['innerBlocks'][2]['innerBlocks'][1]['attrs']['content'] ?? '') === 'guesswork mornings', 'wrapped-stat-definition-list-third-description'); $complex_blocks = html_to_blocks_raw_handler(['HTML' => '
Term
Description

Extra

']); $complex_names = $flatten_block_names($complex_blocks); $assert(\in_array('core/html', $complex_names, \true), 'complex-definition-list-still-falls-back', \implode(', ', $complex_names)); diff --git a/vendor_prefixed/chubes4/html-to-blocks-converter/tests/smoke-ember-rye-media-collage.php b/vendor_prefixed/chubes4/html-to-blocks-converter/tests/smoke-ember-rye-media-collage.php new file mode 100644 index 0000000..b6b0a2c --- /dev/null +++ b/vendor_prefixed/chubes4/html-to-blocks-converter/tests/smoke-ember-rye-media-collage.php @@ -0,0 +1,160 @@ + []]; + } + } + \class_alias('BlockFormatBridge\Vendor\WP_Block_Type_Registry', 'WP_Block_Type_Registry', \false); +} +foreach (['esc_attr', 'esc_html', 'esc_url'] as $function_name) { + if (!\function_exists($function_name)) { + eval('function ' . $function_name . '( $value ) { return htmlspecialchars( (string) $value, ENT_QUOTES, "UTF-8" ); }'); + } +} +if (!\function_exists('BlockFormatBridge\Vendor\wp_strip_all_tags')) { + function wp_strip_all_tags($text) + { + return \strip_tags((string) $text); + } +} +if (!\function_exists('BlockFormatBridge\Vendor\get_shortcode_regex')) { + function get_shortcode_regex() + { + return '(?!)'; + } +} +if (!\function_exists('do_action')) { + function do_action($hook_name, ...$args) + { + } +} +if (!\function_exists('BlockFormatBridge\Vendor\serialize_blocks')) { + function serialize_blocks(array $blocks): string + { + $output = ''; + foreach ($blocks as $block) { + $name = $block['blockName'] ?? ''; + if ('core/html' === $name) { + $output .= '' . ($block['attrs']['content'] ?? $block['innerHTML'] ?? '') . ''; + continue; + } + $output .= ''; + $output .= $block['innerContent'][0] ?? $block['innerHTML'] ?? ''; + $output .= serialize_blocks($block['innerBlocks'] ?? []); + $inner_content = $block['innerContent'] ?? []; + $output .= \end($inner_content) ? \end($inner_content) : ''; + $output .= ''; + } + return $output; + } +} +$repo_root = \dirname(__DIR__); +require_once $repo_root . '/includes/class-block-factory.php'; +require_once $repo_root . '/includes/class-attribute-parser.php'; +require_once $repo_root . '/includes/class-html-element.php'; +require_once $repo_root . '/includes/class-transform-registry.php'; +require_once $repo_root . '/raw-handler.php'; +$failures = []; +$assertions = 0; +$assert = static function ($condition, $label, $detail = '') use (&$failures, &$assertions) { + $assertions++; + if (!$condition) { + $failures[] = 'FAIL [' . $label . ']' . ('' !== $detail ? ': ' . $detail : ''); + } +}; +$flatten_blocks = static function (array $blocks) use (&$flatten_blocks): array { + $flat = []; + foreach ($blocks as $block) { + $flat[] = $block; + $flat = \array_merge($flat, $flatten_blocks($block['innerBlocks'] ?? [])); + } + return $flat; +}; +$html = << +
+ Wood-fired pizza with basil and melted mozzarella + Friends sharing food at a warm restaurant table + Fresh pizza topped with herbs +
+HTML; +$blocks = html_to_blocks_raw_handler(['HTML' => $html]); +$flat = $flatten_blocks($blocks); +$serialized = serialize_blocks($blocks); +$names = \array_map(static function ($block) { + return $block['blockName'] ?? ''; +}, $flat); +$groups = \array_values(\array_filter($flat, static function ($block) { + return 'core/group' === ($block['blockName'] ?? ''); +})); +$images = \array_values(\array_filter($flat, static function ($block) { + return 'core/image' === ($block['blockName'] ?? ''); +})); +$class_names = \array_map(static function ($block) { + return $block['attrs']['className'] ?? ''; +}, $flat); +$assert(!\str_contains($serialized, ''), 'ember-rye-fragment-avoids-core-html', $serialized); +$assert(\count($blocks) === 2, 'ember-rye-top-level-block-count', (string) \count($blocks)); +$assert(\in_array('hero-media', $class_names, \true), 'hero-media-class-survives', \implode(', ', $class_names)); +$assert(\in_array('photo-collage reveal', $class_names, \true), 'photo-collage-classes-survive', \implode(', ', $class_names)); +$assert(\count($groups) === 2, 'hero-and-collage-use-group-blocks', \implode(', ', $names)); +$assert(($blocks[0]['attrs']['ariaLabel'] ?? '') === 'A wood-fired pizza coming out of a glowing oven', 'hero-media-aria-label-survives', $serialized); +$assert(($blocks[1]['attrs']['ariaLabel'] ?? '') === 'Restaurant food and dining photography', 'photo-collage-aria-label-survives', $serialized); +$assert(\count($images) === 3, 'photo-collage-has-three-image-blocks', \implode(', ', $names)); +$expected_alts = ['Wood-fired pizza with basil and melted mozzarella', 'Friends sharing food at a warm restaurant table', 'Fresh pizza topped with herbs']; +foreach ($expected_alts as $index => $alt) { + $assert(($images[$index]['attrs']['alt'] ?? '') === $alt, 'photo-collage-alt-' . ($index + 1) . '-survives', $serialized); + $assert(\str_contains($images[$index]['attrs']['url'] ?? '', 'images.unsplash.com/photo-'), 'photo-collage-url-' . ($index + 1) . '-survives', $serialized); +} +echo 'Assertions: ' . $assertions . \PHP_EOL; +if (empty($failures)) { + echo 'ALL PASS' . \PHP_EOL; + exit(0); +} +echo 'FAILURES (' . \count($failures) . '):' . \PHP_EOL; +foreach ($failures as $failure) { + echo ' - ' . $failure . \PHP_EOL; +} +exit(1); diff --git a/vendor_prefixed/chubes4/html-to-blocks-converter/tests/smoke-form-fallback-scope.php b/vendor_prefixed/chubes4/html-to-blocks-converter/tests/smoke-form-fallback-scope.php index 88830a3..694529a 100644 --- a/vendor_prefixed/chubes4/html-to-blocks-converter/tests/smoke-form-fallback-scope.php +++ b/vendor_prefixed/chubes4/html-to-blocks-converter/tests/smoke-form-fallback-scope.php @@ -184,6 +184,54 @@ function serialize_blocks(array $blocks): string $assert(\str_contains($network_search_serialized, '
'), 'extrachill-network-search-form-is-local-core-html-island', $network_search_serialized); $assert(\count($fallback_events) === 1, 'extrachill-network-search-emits-one-local-form-fallback', (string) \count($fallback_events)); $assert(($fallback_events[0][1]['tag_name'] ?? '') === 'FORM', 'extrachill-network-search-fallback-context-is-form', \print_r($fallback_events, \true)); +$eastbank_static_preview_form = <<<'HTML' + + + + + + + + +

Static preview only - bring this information with you or call the shop before visiting.

+
+HTML; +$fallback_events = []; +$eastbank_form_serialized = serialize_blocks(html_to_blocks_raw_handler(['HTML' => $eastbank_static_preview_form])); +$assert(!\str_contains($eastbank_form_serialized, ''), 'eastbank-static-preview-form-avoids-core-html-fallback', $eastbank_form_serialized); +$assert(\str_contains($eastbank_form_serialized, '
'), 'eastbank-static-preview-form-becomes-group', $eastbank_form_serialized); +$assert(\str_contains($eastbank_form_serialized, 'Item'), 'eastbank-static-preview-label-survives', $eastbank_form_serialized); +$assert(\str_contains($eastbank_form_serialized, 'Example: desk lamp, toaster, backpack zipper'), 'eastbank-static-preview-placeholder-survives', $eastbank_form_serialized); +$assert(\str_contains($eastbank_form_serialized, ''), 'eastbank-static-preview-select-becomes-list', $eastbank_form_serialized); +$assert(\str_contains($eastbank_form_serialized, 'Thursday afternoon'), 'eastbank-static-preview-option-survives', $eastbank_form_serialized); +$assert(\str_contains($eastbank_form_serialized, 'Prepare my bench note'), 'eastbank-static-preview-button-survives', $eastbank_form_serialized); +$assert(\str_contains($eastbank_form_serialized, 'Static preview only'), 'eastbank-static-preview-note-survives', $eastbank_form_serialized); +$assert(\count($fallback_events) === 0, 'eastbank-static-preview-emits-no-fallback-event', (string) \count($fallback_events)); +$ember_form_card = <<<'HTML' +

Request a reservation

+HTML; +$fallback_events = []; +$ember_form_card_serialized = serialize_blocks(html_to_blocks_raw_handler(['HTML' => $ember_form_card])); +$assert(!\str_contains($ember_form_card_serialized, ''), 'ember-form-card-avoids-core-html-fallback', $ember_form_card_serialized); +$assert(\str_contains($ember_form_card_serialized, '
'), 'ember-form-card-becomes-group', $ember_form_card_serialized); +$assert(\str_contains($ember_form_card_serialized, 'Request a reservation'), 'ember-form-card-title-survives', $ember_form_card_serialized); +$assert(\str_contains($ember_form_card_serialized, 'Name'), 'ember-form-card-name-label-survives', $ember_form_card_serialized); +$assert(\str_contains($ember_form_card_serialized, 'Your name'), 'ember-form-card-name-placeholder-survives', $ember_form_card_serialized); +$assert(\str_contains($ember_form_card_serialized, 'you@example.com'), 'ember-form-card-email-placeholder-survives', $ember_form_card_serialized); +$assert(\str_contains($ember_form_card_serialized, 'Preferred date'), 'ember-form-card-date-placeholder-survives', $ember_form_card_serialized); +$assert(\str_contains($ember_form_card_serialized, ''), 'ember-form-card-select-becomes-list', $ember_form_card_serialized); +$assert(\str_contains($ember_form_card_serialized, '5:00 PM'), 'ember-form-card-first-option-survives', $ember_form_card_serialized); +$assert(\str_contains($ember_form_card_serialized, 'Request Table'), 'ember-form-card-submit-text-survives', $ember_form_card_serialized); +$assert(\count($fallback_events) === 0, 'ember-form-card-emits-no-fallback-event', (string) \count($fallback_events)); +$untargeted_real_form = '
'; +$fallback_events = []; +$untargeted_real_form_serialized = serialize_blocks(html_to_blocks_raw_handler(['HTML' => $untargeted_real_form])); +$assert(\str_contains($untargeted_real_form_serialized, '
'), 'untargeted-real-form-still-falls-back', $untargeted_real_form_serialized); +$assert(\count($fallback_events) === 1, 'untargeted-real-form-emits-fallback-event', (string) \count($fallback_events)); echo 'Assertions: ' . $assertions . \PHP_EOL; if (empty($failures)) { echo 'ALL PASS' . \PHP_EOL; diff --git a/vendor_prefixed/chubes4/html-to-blocks-converter/tests/smoke-loom-larder-fallbacks.php b/vendor_prefixed/chubes4/html-to-blocks-converter/tests/smoke-loom-larder-fallbacks.php new file mode 100644 index 0000000..dac790e --- /dev/null +++ b/vendor_prefixed/chubes4/html-to-blocks-converter/tests/smoke-loom-larder-fallbacks.php @@ -0,0 +1,181 @@ + []]; + } + } + \class_alias('BlockFormatBridge\Vendor\WP_Block_Type_Registry', 'WP_Block_Type_Registry', \false); +} +foreach (['esc_attr', 'esc_html', 'esc_url'] as $function_name) { + if (!\function_exists($function_name)) { + eval('function ' . $function_name . '( $value ) { return htmlspecialchars( (string) $value, ENT_QUOTES, "UTF-8" ); }'); + } +} +if (!\function_exists('BlockFormatBridge\Vendor\wp_strip_all_tags')) { + function wp_strip_all_tags($text) + { + return \strip_tags((string) $text); + } +} +if (!\function_exists('BlockFormatBridge\Vendor\get_shortcode_regex')) { + function get_shortcode_regex() + { + return '(?!)'; + } +} +$fallback_events = []; +if (!\function_exists('do_action')) { + function do_action($hook_name, ...$args) + { + global $fallback_events; + if ('html_to_blocks_unsupported_html_fallback' === $hook_name) { + $fallback_events[] = $args; + } + } +} +if (!\function_exists('BlockFormatBridge\Vendor\serialize_blocks')) { + function serialize_blocks(array $blocks): string + { + $output = ''; + foreach ($blocks as $block) { + $name = $block['blockName'] ?? ''; + $attrs = \array_diff_key($block['attrs'] ?? [], ['content' => \true, 'svg' => \true, 'metadata' => \true]); + $attrs_json = empty($attrs) ? '' : ' ' . \json_encode($attrs, \JSON_UNESCAPED_SLASHES); + if ('core/html' === $name) { + $output .= '' . ($block['attrs']['content'] ?? $block['innerHTML'] ?? '') . ''; + continue; + } + $output .= ''; + $output .= $block['innerContent'][0] ?? $block['innerHTML'] ?? ''; + $output .= serialize_blocks($block['innerBlocks'] ?? []); + $inner_content = $block['innerContent'] ?? []; + $output .= \end($inner_content) ? \end($inner_content) : ''; + $output .= ''; + } + return $output; + } +} +$repo_root = \dirname(__DIR__); +require_once $repo_root . '/includes/class-block-factory.php'; +require_once $repo_root . '/includes/class-attribute-parser.php'; +require_once $repo_root . '/includes/class-html-element.php'; +require_once $repo_root . '/includes/class-svg-icon-classifier.php'; +require_once $repo_root . '/includes/class-transform-registry.php'; +require_once $repo_root . '/raw-handler.php'; +$failures = []; +$assertions = 0; +$assert = static function ($condition, $label, $detail = '') use (&$failures, &$assertions) { + $assertions++; + if (!$condition) { + $failures[] = 'FAIL [' . $label . ']' . ('' !== $detail ? ': ' . $detail : ''); + } +}; +$collect_blocks = static function (array $blocks, string $name) use (&$collect_blocks): array { + $matches = []; + foreach ($blocks as $block) { + if (($block['blockName'] ?? '') === $name) { + $matches[] = $block; + } + if (!empty($block['innerBlocks']) && \is_array($block['innerBlocks'])) { + $matches = \array_merge($matches, $collect_blocks($block['innerBlocks'], $name)); + } + } + return $matches; +}; +$flatten_class_names = static function (array $blocks) use (&$flatten_class_names): array { + $class_names = []; + foreach ($blocks as $block) { + if (!empty($block['attrs']['className'])) { + $class_names[] = $block['attrs']['className']; + } + $class_names = \array_merge($class_names, $flatten_class_names($block['innerBlocks'] ?? [])); + } + return $class_names; +}; +$svg_html = <<<'HTML' + + Folded handwoven bread cloth with visible selvedge + + + + + + + + + + + + +HTML; +$patch_html = <<<'HTML' + +HTML; +$svg_blocks = html_to_blocks_raw_handler(['HTML' => $svg_html]); +$patch_blocks = html_to_blocks_raw_handler(['HTML' => $patch_html]); +$serialized = serialize_blocks(\array_merge($svg_blocks, $patch_blocks)); +$fallbacks = \array_merge($collect_blocks($svg_blocks, 'core/html'), $collect_blocks($patch_blocks, 'core/html')); +$svg_placeholds = $collect_blocks($svg_blocks, 'html-to-blocks/svg-icon'); +$class_names = $flatten_class_names($patch_blocks); +$assert(\count($fallbacks) === 0, 'loom-larder-fragments-do-not-use-core-html', $serialized); +$assert(\count($fallback_events) === 0, 'loom-larder-fragments-emit-no-fallback-events', (string) \count($fallback_events)); +$assert(\count($svg_placeholds) === 1, 'cloth-svg-becomes-placeholder', $serialized); +$assert(($svg_placeholds[0]['attrs']['metadata']['kind'] ?? '') === 'inline-svg-illustration', 'cloth-svg-is-classified-as-illustration', \var_export($svg_placeholds[0]['attrs']['metadata'] ?? [], \true)); +$assert(\str_contains($svg_placeholds[0]['attrs']['svg'] ?? '', ''; - $output .= $block['innerContent'][0] ?? $block['innerHTML'] ?? ''; - $output .= serialize_blocks($block['innerBlocks'] ?? []); + $inner_blocks = $block['innerBlocks'] ?? []; $inner_content = $block['innerContent'] ?? []; - $output .= \end($inner_content) ? \end($inner_content) : ''; + $inner_index = 0; + if ([] === $inner_content) { + $output .= $block['innerHTML'] ?? ''; + } else { + foreach ($inner_content as $content) { + if (null === $content) { + $output .= serialize_blocks([$inner_blocks[$inner_index] ?? []]); + $inner_index++; + continue; + } + $output .= $content; + } + } $output .= ''; } return $output; } } +if (!\function_exists('BlockFormatBridge\Vendor\html_to_blocks_smoke_block_names')) { + function html_to_blocks_smoke_block_names(array $blocks): array + { + $names = []; + foreach ($blocks as $block) { + $names[] = $block['blockName'] ?? ''; + $names = \array_merge($names, html_to_blocks_smoke_block_names($block['innerBlocks'] ?? [])); + } + return $names; + } +} $repo_root = \dirname(__DIR__); require_once $repo_root . '/includes/class-block-factory.php'; require_once $repo_root . '/includes/class-attribute-parser.php'; @@ -186,6 +215,71 @@ function serialize_blocks(array $blocks): string $assert(!\str_contains($parsed_decorative_inline_serialized, ''), 'parsed-decorative-inline-spans-avoid-core-html-fallback', $parsed_decorative_inline_serialized); $assert(\str_contains($parsed_decorative_inline_serialized, ''), 'parsed-decorative-inline-class-dot-survives', $parsed_decorative_inline_serialized); $assert(\str_contains($parsed_decorative_inline_serialized, 'width:6px;height:6px;border-radius:50%;background:var(--accent);display:inline-block;'), 'parsed-decorative-inline-style-dot-survives', $parsed_decorative_inline_serialized); +$ember_nav_serialized = serialize_blocks(html_to_blocks_raw_handler(['HTML' => ''])); +$assert(!\str_contains($ember_nav_serialized, ''), 'ember-nav-avoids-core-html-fallback', $ember_nav_serialized); +$assert(\str_contains($ember_nav_serialized, '
+HTML; +$ember_faq_blocks = html_to_blocks_raw_handler(['HTML' => $ember_faq_html]); +$ember_faq_names = html_to_blocks_smoke_block_names($ember_faq_blocks); +$ember_faq_serialized = serialize_blocks($ember_faq_blocks); +$ember_faq_first = $ember_faq_blocks[0]['innerBlocks'][0] ?? []; +$assert(!\in_array('core/html', $ember_faq_names, \true), 'ember-faq-avoids-core-html-blocks', \implode(', ', $ember_faq_names)); +$assert(!\str_contains($ember_faq_serialized, ''), 'ember-faq-serialized-has-no-wp-html', $ember_faq_serialized); +$assert(\str_contains($ember_faq_serialized, 'faq-list reveal'), 'ember-faq-wrapper-class-survives', $ember_faq_serialized); +$assert(\true === ($ember_faq_first['attrs']['showContent'] ?? \false), 'ember-faq-open-state-survives', \var_export($ember_faq_first['attrs'] ?? [], \true)); +$assert(\str_contains($ember_faq_serialized, 'Do you take walk-ins?'), 'ember-faq-first-summary-survives', $ember_faq_serialized); +$assert(\str_contains($ember_faq_serialized, 'Do you offer takeout?'), 'ember-faq-second-summary-survives', $ember_faq_serialized); +$assert(\str_contains($ember_faq_serialized, 'bar and patio seating'), 'ember-faq-first-answer-survives', $ember_faq_serialized); +$assert(\str_contains($ember_faq_serialized, 'depending on oven volume'), 'ember-faq-second-answer-survives', $ember_faq_serialized); echo 'Assertions: ' . $assertions . \PHP_EOL; if (empty($failures)) { echo 'ALL PASS' . \PHP_EOL;