diff --git a/README.md b/README.md index 72c7f6f..27d2e5a 100644 --- a/README.md +++ b/README.md @@ -37,10 +37,12 @@ interface BFB_Format_Adapter { public function slug(): string; public function to_blocks( string $content, array $options = array() ): array; public function from_blocks( array $blocks, array $options = array() ): string; - public function detect( string $content ): bool; // reserved for future use } ``` +BFB is a declared-format conversion API. Callers pass the source format explicitly to `bfb_convert()` or +`bfb_to_blocks()`; BFB does not silently guess between HTML, Blocks, Markdown, or custom adapters. + BFB includes two adapters: - **`BFB_HTML_Adapter`** — `to_blocks()` delegates to `html_to_blocks_raw_handler()` from `html-to-blocks-converter`; @@ -119,9 +121,9 @@ Rules: - Missing or malformed marker values should fall back to the normal HTML conversion path rather than guessing. The marker contract belongs in BFB because BFB is the public conversion substrate. The runtime HTML-element transforms -belong in h2bc because `BFB_HTML_Adapter::to_blocks()` delegates HTML → Blocks conversion to -`html_to_blocks_raw_handler()`. BFB will inherit marker support after h2bc implements those explicit raw transforms and -the bundled dependency is refreshed. +are currently supplied by h2bc through BFB's bundled dependency, and BFB verifies those markers through the public +`bfb_convert( $html, 'html', 'blocks' )` path. h2bc should treat these attributes as an explicit shared extension +contract with BFB, not as a license to infer Site Editor primitives from unmarked HTML. ## Install diff --git a/docs/block-theme-compiler-surface.md b/docs/block-theme-compiler-surface.md index 3d995ab..0a0a660 100644 --- a/docs/block-theme-compiler-surface.md +++ b/docs/block-theme-compiler-surface.md @@ -44,8 +44,9 @@ The deterministic targets are: avoid implying a WordPress core contract that does not exist. Implementation note: the marker contract is documented here because BFB owns the public conversion substrate. The actual -HTML-element transforms belong in html-to-blocks-converter, the library BFB delegates to for HTML → Blocks. BFB should not -add a parallel pre-parser around h2bc for these markers. +HTML-element transforms currently live in html-to-blocks-converter, the library BFB delegates to for HTML → Blocks. That +is a shared extension contract: h2bc may recognize these explicitly documented BFB markers, while BFB verifies marker +behavior through `bfb_convert( $html, 'html', 'blocks' )` instead of adding a parallel pre-parser around h2bc. ## Answers diff --git a/docs/block-theme-conversion-workflow.md b/docs/block-theme-conversion-workflow.md index 632c5e4..4bfb492 100644 --- a/docs/block-theme-conversion-workflow.md +++ b/docs/block-theme-conversion-workflow.md @@ -61,7 +61,8 @@ h2bc should: - Generate a core-block inventory and classification map from WordPress/Gutenberg `block.json` metadata. - Keep the generated coverage documentation in sync with that map. - Implement raw transforms when source HTML carries enough signal to preserve the author's intent. -- Implement explicit BFB marker transforms for primitives such as pattern and template-part references. +- Implement explicit shared marker transforms for primitives such as pattern and template-part references when BFB + documents the public marker vocabulary. - Fall back safely when markup is ambiguous. h2bc should not: @@ -74,6 +75,7 @@ h2bc should not: Tracking: - h2bc #56: https://github.com/chubes4/html-to-blocks-converter/issues/56 +- h2bc #418: https://github.com/chubes4/html-to-blocks-converter/issues/418 - h2bc #55: https://github.com/chubes4/html-to-blocks-converter/issues/55 ## BFB Responsibilities @@ -91,6 +93,7 @@ BFB should: - Expose ability operations for machine callers, including conversion and capability reporting. - Keep WP-CLI ergonomics thin and script-friendly for humans and shell-based tools. - Report what the active substrate supports so compiler consumers can plan fallbacks. +- Consume h2bc's public capability API when available instead of reflecting over h2bc internals. BFB should not: diff --git a/includes/api.php b/includes/api.php index 233ab3f..f80f2cd 100644 --- a/includes/api.php +++ b/includes/api.php @@ -60,6 +60,16 @@ function bfb_capabilities(): array { $h2bc = bfb_h2bc_capabilities(); + $block_coverage = isset( $h2bc['inventory']['block_coverage'] ) && is_array( $h2bc['inventory']['block_coverage'] ) + ? $h2bc['inventory']['block_coverage'] + : array( + 'source' => (string) ( $h2bc['inventory']['source'] ?? 'h2bc_capability_api_missing' ), + 'requires' => (string) ( $h2bc['inventory']['requires'] ?? 'https://github.com/chubes4/html-to-blocks-converter/issues/418' ), + 'supported_blocks' => array(), + 'unsupported_blocks' => array(), + 'classifications' => array(), + ); + return array( 'bridge' => array( 'version' => defined( 'BFB_VERSION' ) ? BFB_VERSION : null, @@ -77,13 +87,7 @@ function bfb_capabilities(): array { ), ), 'h2bc' => $h2bc, - 'block_coverage' => array( - 'source' => 'not_available', - 'requires' => 'h2bc#56', - 'supported_blocks' => array(), - 'unsupported_blocks' => array(), - 'classifications' => array(), - ), + 'block_coverage' => $block_coverage, 'hooks' => array( 'filters' => array( 'bfb_register_format_adapter', @@ -214,19 +218,145 @@ function bfb_h2bc_capabilities(): array { } } + $capability_function = bfb_h2bc_capability_function(); + $inventory = array( + 'source' => null !== $capability_function ? 'h2bc_capabilities' : 'h2bc_capability_api_missing', + 'requires' => null !== $capability_function ? null : 'https://github.com/chubes4/html-to-blocks-converter/issues/418', + ); + + if ( null !== $capability_function ) { + $capability_report = $capability_function(); + if ( is_array( $capability_report ) ) { + $inventory = bfb_normalize_h2bc_inventory( $capability_report ); + if ( isset( $inventory['version'] ) && is_string( $inventory['version'] ) && '' !== $inventory['version'] ) { + $version = $inventory['version']; + } + } + } + return array( - 'available' => null !== $handler, - 'version' => $version, - 'path' => $path, - 'raw_handler' => $handler, - 'inventory' => array( - 'source' => 'not_available', - 'requires' => 'h2bc#56', + 'available' => null !== $handler, + 'version' => $version, + 'path' => $path, + 'raw_handler' => $handler, + 'capability_api' => $capability_function, + 'inventory' => $inventory, + ); + } +} + +if ( ! function_exists( 'bfb_h2bc_capability_function' ) ) { + /** + * Resolve h2bc's public capability function when the active substrate exposes one. + * + * @return callable-string|null Callable function name, or null when h2bc lacks the API. + */ + function bfb_h2bc_capability_function(): ?string { + $candidates = array( + '\BlockFormatBridge\Vendor\html_to_blocks_capabilities', + 'html_to_blocks_capabilities', + ); + $defined = get_defined_functions(); + $functions = array_map( 'strtolower', $defined['user'] ); + + foreach ( $candidates as $candidate ) { + if ( in_array( strtolower( ltrim( $candidate, '\\' ) ), $functions, true ) ) { + /** @var callable-string $candidate */ + return $candidate; + } + } + + return null; + } +} + +if ( ! function_exists( 'bfb_normalize_h2bc_inventory' ) ) { + /** + * Normalize h2bc-owned capability data into BFB's public capability shape. + * + * @param array $report h2bc capability report. + * @return array + */ + function bfb_normalize_h2bc_inventory( array $report ): array { + $block_coverage = isset( $report['block_coverage'] ) && is_array( $report['block_coverage'] ) ? $report['block_coverage'] : array(); + $transforms = isset( $report['transforms'] ) && is_array( $report['transforms'] ) ? $report['transforms'] : array(); + + $supported_blocks = bfb_h2bc_report_list( $report, $block_coverage, 'supported_blocks' ); + $unsupported_blocks = bfb_h2bc_report_list( $report, $block_coverage, 'unsupported_blocks' ); + $classifications = bfb_h2bc_report_array( $report, $block_coverage, 'classifications' ); + $families = bfb_h2bc_report_list( $report, $transforms, 'families' ); + + return array( + 'source' => 'h2bc_capabilities', + 'version' => isset( $report['version'] ) && is_scalar( $report['version'] ) ? (string) $report['version'] : null, + 'handler' => isset( $report['handler'] ) && is_scalar( $report['handler'] ) ? (string) $report['handler'] : null, + 'transform_families' => $families, + 'block_coverage' => array( + 'source' => 'h2bc_capabilities', + 'supported_blocks' => $supported_blocks, + 'unsupported_blocks' => $unsupported_blocks, + 'classifications' => $classifications, ), + 'raw' => $report, ); } } +if ( ! function_exists( 'bfb_h2bc_report_list' ) ) { + /** + * Return a normalized scalar list from possible report locations. + * + * @param array $primary Primary report data. + * @param array $secondary Secondary report data. + * @param string $key Field key. + * @return array + */ + function bfb_h2bc_report_list( array $primary, array $secondary, string $key ): array { + $values = array(); + if ( isset( $primary[ $key ] ) && is_array( $primary[ $key ] ) ) { + $values = $primary[ $key ]; + } elseif ( isset( $secondary[ $key ] ) && is_array( $secondary[ $key ] ) ) { + $values = $secondary[ $key ]; + } + + return array_values( + array_filter( + array_map( + static function ( $value ): string { + return is_scalar( $value ) ? (string) $value : ''; + }, + $values + ), + static function ( string $value ): bool { + return '' !== $value; + } + ) + ); + } +} + +if ( ! function_exists( 'bfb_h2bc_report_array' ) ) { + /** + * Return an array field from possible report locations. + * + * @param array $primary Primary report data. + * @param array $secondary Secondary report data. + * @param string $key Field key. + * @return array + */ + function bfb_h2bc_report_array( array $primary, array $secondary, string $key ): array { + if ( isset( $primary[ $key ] ) && is_array( $primary[ $key ] ) ) { + return $primary[ $key ]; + } + + if ( isset( $secondary[ $key ] ) && is_array( $secondary[ $key ] ) ) { + return $secondary[ $key ]; + } + + return array(); + } +} + if ( ! function_exists( 'bfb_get_adapter' ) ) { /** * Resolve an adapter by slug. @@ -864,10 +994,6 @@ function bfb_render_post( $post, string $format, array $options = array() ): str return ''; } - if ( ! $post_obj instanceof WP_Post ) { - return ''; - } - $content = (string) $post_obj->post_content; if ( '' === $content ) { return ''; diff --git a/includes/class-bfb-html-adapter.php b/includes/class-bfb-html-adapter.php index 4ff31f7..551993e 100644 --- a/includes/class-bfb-html-adapter.php +++ b/includes/class-bfb-html-adapter.php @@ -115,13 +115,4 @@ public function from_blocks( array $blocks, array $options = array() ): string { } return $html; } - - /** - * @inheritDoc - */ - public function detect( string $content ): bool { - // Reserved for future use. v0.1.0 doesn't auto-detect. - unset( $content ); - return false; - } } diff --git a/includes/class-bfb-markdown-adapter.php b/includes/class-bfb-markdown-adapter.php index 37456c0..08293c1 100644 --- a/includes/class-bfb-markdown-adapter.php +++ b/includes/class-bfb-markdown-adapter.php @@ -139,15 +139,6 @@ static function ( array $pre_match ): string { return (string) apply_filters( 'bfb_markdown_output', $markdown, $html, $blocks ); } - /** - * @inheritDoc - */ - public function detect( string $content ): bool { - // Reserved for future use. v0.1.0 doesn't auto-detect. - unset( $content ); - return false; - } - /** * Render markdown to HTML using league/commonmark with GFM extensions. * diff --git a/includes/interface-bfb-format-adapter.php b/includes/interface-bfb-format-adapter.php index 952b3e0..ce657e8 100644 --- a/includes/interface-bfb-format-adapter.php +++ b/includes/interface-bfb-format-adapter.php @@ -53,17 +53,4 @@ public function to_blocks( string $content, array $options = array() ): array; * @return string Content in this adapter's format. */ public function from_blocks( array $blocks, array $options = array() ): string; - - /** - * Best-effort detection of whether $content is in this format. - * - * Reserved for future use. v0.1.0 does not consult detect() from - * any production path — auto-detection is opt-in via filters and - * per-call hints. Implementations may return false until the - * detection rules are designed. - * - * @param string $content Content to test. - * @return bool - */ - public function detect( string $content ): bool; } diff --git a/tests/BFBConversionUnitTest.php b/tests/BFBConversionUnitTest.php index 283a52f..1a58b9d 100644 --- a/tests/BFBConversionUnitTest.php +++ b/tests/BFBConversionUnitTest.php @@ -100,6 +100,26 @@ function_exists( '\BlockFormatBridge\Vendor\html_to_blocks_raw_handler' ), } } + /** + * Explicit Site Editor markers should convert through the public BFB path. + */ + public function test_site_editor_markers_convert_through_bfb_convert(): void { + $pattern = $this->blocks_from( '

Pricing

', 'html' ); + $this->assertSame( 'core/pattern', $pattern[0]['blockName'] ?? null, 'Pattern marker should convert to core/pattern.' ); + $this->assertSame( 'theme/pricing-table', $pattern[0]['attrs']['slug'] ?? null, 'Pattern marker should preserve the explicit slug.' ); + + $template_part = $this->blocks_from( '

Site

', 'html' ); + $this->assertSame( 'core/template-part', $template_part[0]['blockName'] ?? null, 'Template-part marker should convert to core/template-part.' ); + $this->assertSame( 'header', $template_part[0]['attrs']['slug'] ?? null, 'Template-part marker should preserve the explicit slug.' ); + $this->assertSame( 'header', $template_part[0]['attrs']['area'] ?? null, 'Standard template-part areas should set area.' ); + + $invalid_pattern = $this->blocks_from( '

Pricing

', 'html' ); + $this->assertNotSame( 'core/pattern', $invalid_pattern[0]['blockName'] ?? null, 'Invalid pattern marker should fall through to normal HTML conversion.' ); + + $unmarked_header = $this->blocks_from( '

Site

', 'html' ); + $this->assertNotSame( 'core/template-part', $unmarked_header[0]['blockName'] ?? null, 'Unmarked header should not be inferred as a template part.' ); + } + /** * Conversion options should flow from the public API into adapters and h2bc args. */ @@ -199,10 +219,6 @@ public function from_blocks( array $blocks, array $options = array() ): string { return 'probe'; } - public function detect( string $content ): bool { - unset( $content ); - return false; - } }; $adapter_filter = static function ( $adapter, string $slug ) use ( $probe ) { diff --git a/tests/smoke-capabilities-abilities.php b/tests/smoke-capabilities-abilities.php index 38d3d0f..dd2271d 100644 --- a/tests/smoke-capabilities-abilities.php +++ b/tests/smoke-capabilities-abilities.php @@ -146,8 +146,8 @@ function is_wp_error( $value ): bool { bfb_capabilities_smoke_assert( isset( $report['formats']['html'] ), 'Capability report should expose registered adapters.' ); bfb_capabilities_smoke_assert( false === $report['formats']['html']['pivot'], 'Adapter formats should not be marked as pivot formats.' ); bfb_capabilities_smoke_assert( isset( $report['conversions']['html_to_blocks'] ), 'Capability report should expose HTML to blocks availability.' ); -bfb_capabilities_smoke_assert( 'not_available' === $report['block_coverage']['source'], 'Capability report should include conservative block coverage placeholder.' ); -bfb_capabilities_smoke_assert( 'h2bc#56' === $report['block_coverage']['requires'], 'Capability report should point at the h2bc inventory follow-up.' ); +bfb_capabilities_smoke_assert( 'h2bc_capability_api_missing' === $report['block_coverage']['source'], 'Capability report should identify the missing h2bc capability API.' ); +bfb_capabilities_smoke_assert( 'https://github.com/chubes4/html-to-blocks-converter/issues/418' === $report['block_coverage']['requires'], 'Capability report should point at the h2bc capability API dependency.' ); bfb_capabilities_smoke_assert( in_array( 'bfb_html_to_blocks_args', $report['hooks']['filters'], true ), 'Capability report should list HTML raw-handler args filter.' ); bfb_capabilities_smoke_assert( in_array( 'bfb_diagnostic', $report['hooks']['actions'], true ), 'Capability report should list observability hooks.' ); bfb_capabilities_smoke_assert( in_array( 'block-format-bridge/get-capabilities', $report['abilities'], true ), 'Capability report should list the capabilities ability.' ); diff --git a/tests/smoke-h2bc-capability-discovery-prefixed.php b/tests/smoke-h2bc-capability-discovery-prefixed.php new file mode 100644 index 0000000..de703b1 --- /dev/null +++ b/tests/smoke-h2bc-capability-discovery-prefixed.php @@ -0,0 +1,57 @@ + 'prefixed-test-version', + 'handler' => __FUNCTION__, + 'transforms' => array( + 'families' => array( 'site-editor', 'text' ), + ), + 'block_coverage' => array( + 'supported_blocks' => array( 'core/pattern', 'core/template-part' ), + 'unsupported_blocks' => array( 'core/query' ), + 'classifications' => array( + 'core/pattern' => 'explicit-marker', + 'core/query' => 'compiler-only', + ), + ), + ); + } +} + +namespace { + if ( ! defined( 'ABSPATH' ) ) { + define( 'ABSPATH', __DIR__ . '/' ); + } + + function trailingslashit( string $path ): string { + return rtrim( $path, '/\\' ) . '/'; + } + + function bfb_h2bc_capability_discovery_assert( bool $condition, string $message ): void { + if ( ! $condition ) { + fwrite( STDERR, "FAIL: {$message}\n" ); + exit( 1 ); + } + } + + require_once __DIR__ . '/../includes/api.php'; + + $report = bfb_h2bc_capabilities(); + + bfb_h2bc_capability_discovery_assert( '\\BlockFormatBridge\\Vendor\\html_to_blocks_capabilities' === $report['capability_api'], 'Prefixed h2bc capability function should be selected.' ); + bfb_h2bc_capability_discovery_assert( 'prefixed-test-version' === $report['version'], 'h2bc version should come from prefixed capability report.' ); + bfb_h2bc_capability_discovery_assert( 'h2bc_capabilities' === $report['inventory']['source'], 'Inventory should identify h2bc capabilities as the source.' ); + bfb_h2bc_capability_discovery_assert( in_array( 'core/pattern', $report['inventory']['block_coverage']['supported_blocks'], true ), 'Supported blocks should come from h2bc.' ); + bfb_h2bc_capability_discovery_assert( 'explicit-marker' === $report['inventory']['block_coverage']['classifications']['core/pattern'], 'Classifications should come from h2bc.' ); + + echo "PASS: prefixed h2bc capability discovery\n"; +} diff --git a/tests/smoke-h2bc-capability-discovery-standalone.php b/tests/smoke-h2bc-capability-discovery-standalone.php new file mode 100644 index 0000000..9b7a6ca --- /dev/null +++ b/tests/smoke-h2bc-capability-discovery-standalone.php @@ -0,0 +1,46 @@ + 'standalone-test-version', + 'handler' => __FUNCTION__, + 'supported_blocks' => array( 'core/heading', 'core/paragraph' ), + 'classifications' => array( + 'core/heading' => 'raw-transformable', + ), + ); +} + +function trailingslashit( string $path ): string { + return rtrim( $path, '/\\' ) . '/'; +} + +function bfb_h2bc_standalone_capability_assert( bool $condition, string $message ): void { + if ( ! $condition ) { + fwrite( STDERR, "FAIL: {$message}\n" ); + exit( 1 ); + } +} + +require_once __DIR__ . '/../includes/api.php'; + +$report = bfb_h2bc_capabilities(); + +bfb_h2bc_standalone_capability_assert( 'html_to_blocks_capabilities' === $report['capability_api'], 'Standalone h2bc capability function should be selected.' ); +bfb_h2bc_standalone_capability_assert( 'standalone-test-version' === $report['version'], 'h2bc version should come from standalone capability report.' ); +bfb_h2bc_standalone_capability_assert( 'h2bc_capabilities' === $report['inventory']['source'], 'Inventory should identify h2bc capabilities as the source.' ); +bfb_h2bc_standalone_capability_assert( in_array( 'core/heading', $report['inventory']['block_coverage']['supported_blocks'], true ), 'Supported blocks should come from h2bc.' ); +bfb_h2bc_standalone_capability_assert( 'raw-transformable' === $report['inventory']['block_coverage']['classifications']['core/heading'], 'Classifications should come from h2bc.' ); + +echo "PASS: standalone h2bc capability discovery\n"; diff --git a/tests/smoke-multi-consumer-bundled-load.php b/tests/smoke-multi-consumer-bundled-load.php index 3252da8..fe7221e 100644 --- a/tests/smoke-multi-consumer-bundled-load.php +++ b/tests/smoke-multi-consumer-bundled-load.php @@ -98,6 +98,10 @@ function do_action( string $hook_name, ...$args ): void { bfb_smoke_do_action_range( $hook_name, null, null, $args ); } +function wp_delete_file( string $file ): bool { + return unlink( $file ); +} + function bfb_smoke_do_action_range( string $hook_name, ?int $min_priority, ?int $max_priority, array $args = array() ): void { $GLOBALS['bfb_smoke_current_action'] = $hook_name;