Skip to content

fix: add a server-side path from WP block markup to rendered HTML - #689

Merged
ViewFromTheBox merged 2 commits into
release/1.5.5from
bugfix/688-server-side-wp-block-markup-to-html
Aug 2, 2026
Merged

ViewFromTheBox merged 2 commits into
release/1.5.5from
bugfix/688-server-side-wp-block-markup-to-html

Conversation

@ViewFromTheBox

@ViewFromTheBox ViewFromTheBox commented Aug 2, 2026 •

Copy link
Copy Markdown
Member

Description

There was no server-side path from WP-serialized block markup to rendered HTML, which made a block theme's templates/*.html and parts/*.html unrenderable outside the editor.

The gap was not just a shape mismatch. Gutenberg persists most block text in the saved HTML, not in the delimiter JSON — 0 of the artisanpack-ui theme's 130 paragraph/heading blocks serialize a content attribute. So a key-rename conversion produces a structurally correct, completely textless page.

This adds BlockMarkupHydrator, which parses the markup and replays each registered block type's block.json attribute definitions back over its saved HTML to recover the content. Recovery is driven entirely by the block-type registry rather than a hand-maintained per-block table, so a block that ships a new sourced attribute is picked up with no change here — which is exactly why this lives beside the block manifests and partials rather than in cms-framework or a host app.

Closes: #688

Type of Change

  • Bug fix (fixes an issue)

Related Issue

Issue: #688

Companion (not blocking, see below): ArtisanPack-UI/cms-framework#274

Motivation and Context

Reproductions from the issue, all of which previously produced empty or no output, now render:

// text only in innerHTML — what theme markup actually gives you
app( BlockRenderer::class )->renderMarkup(
    '<!-- wp:artisanpack/paragraph {"textColor":"text-muted"} -->'
    . '<p class="has-text-muted-color has-text-color">Supporting subheading.</p>'
    . '<!-- /wp:artisanpack/paragraph -->'
);
// => <p class="wp-block-paragraph has-text-muted-color ...">Supporting subheading.</p>

Changes Made

  • BlockMarkupHydrator — the public entry point. hydrate( string $markup ) for raw markup; hydrateTree( array $parsed ) for callers that already have parse_blocks() output (accepts the editor shape too, so it is safe over a mixed tree). Carries a depth cap and a documented trust boundary.
  • BlockAttributeSourceResolver — server-side port of Gutenberg's save-shape matchers (attribute, html incl. multiline, rich-text, text, tag, query, raw) over DOMDocument + XPath.
  • CssSelectorToXPath — hand-rolled translator for the CSS subset the manifests actually use, so the package gains no new Composer dependency for a feature this narrow. Unsupported selectors throw and are reported once per process rather than silently mis-matching.
  • BlockRenderer::renderMarkup( $markup, $defaultTheme ) — markup in, HTML out. Runs template-part and pattern inlining before the walk so a standalone theme template renders its parts as real content rather than empty wrappers.
  • TemplatePartInliner::findPartBlocks() — falls back to hydrating the part's raw markup when it resolves with empty blocks, which is the .html-sourced theme-file case. This closes the second impact bullet in No server-side path from WP block markup to rendered HTML (blocks block-theme rendering) #688 without waiting on cms-framework#274.
  • Dropped the leading backslash from TemplatePartInliner::RESOLVER_CLASS: with it, app() treated the string as a distinct container key and silently ignored any host- or test-supplied override.

Deliberate design decisions

  • Delimiter attributes win over recovered ones. Gutenberg lets the sourced value win outright. Layering recovery underneath means this can only ever fill gaps, never blank a value an editor-persisted tree already had. Verified there is no real theme-markup case where this yields a worse result: Gutenberg's serializer omits any attribute equal to its block.json default, so the dangerous shape (a delimiter carrying "content":"" over populated HTML) is not reachable from editor-produced markup.
  • innerHTML is preserved on the hydrated block. A block whose content lives only in innerHTML and which declares no sourced attributes (core/html) would otherwise hydrate to an empty node, silently losing content.
  • renderMarkup() cannot resolve entity-dependent blocks (post-*, core/query, comments, breadcrumbs) because a string input carries no post. For a full-fidelity render, hydrate to a tree and hand it to <x-ve-blocks :tree :post />, which runs the complete pipeline. Documented on the method.

How Has This Been Tested?

Testing Environment:

  • Operating System: macOS (Darwin 27.0.0)
  • Browser (if applicable): n/a — server-side rendering only
  • PHP Version: 8.4.3
  • Project Version: 1.5.4 → targeting release/1.5.5

Tests Performed:

  1. Full PHP suite: 1626 tests / 4347 assertions, 0 failures.
  2. New suites cover the recovery matrix against the real bundled manifests: paragraph/heading content, inline formatting preservation, button text + href + target + rel, image src/alt/caption/href, multiline list values, quote value + citation, boolean attribute presence semantics, query sources (table cells), multibyte round-trip, inner-block recursion, and the depth cap.
  3. Verified the modern container-block forms behave correctly: a list whose items are list-item inner blocks must leave values empty, or the partial would double-render every item.
  4. Data-driven guard translates every selector in all 105 bundled block.json manifests, so a future manifest introducing an unsupported selector fails CI instead of degrading silently at render time.
  5. Template-part inlining verified end-to-end against a .html-sourced part that resolves with empty blocks.

Accessibility Tests Run

  • Keyboard navigation tested
  • Screen reader tested
  • Color contrast verified
  • ARIA labels checked

Details: Not applicable — this change is entirely server-side block-tree construction and adds no UI, markup structure, or interactive surface. The HTML emitted is produced by the pre-existing block partials, unchanged by this PR; the change only means those partials now receive the text they were always meant to render. If anything it is an a11y improvement: headings and link text that previously rendered empty now carry their real content.

Tests Added

  • Unit tests added/updated
  • Integration tests added/updated
  • All tests passing

Test details: 43 new tests across tests/Unit/VisualEditor/Support/BlockMarkupHydratorTest.php, tests/Unit/VisualEditor/Support/CssSelectorToXPathTest.php, tests/Unit/VisualEditor/Resources/TemplatePartInlinerRawContentTest.php, and packages/visual-editor-renderer-blade/tests/Feature/RenderMarkupTest.php.

Documentation

  • Inline code documentation added
  • README updated
  • Wiki updated
  • API documentation updated

Documentation details: Each new class carries a full docblock covering the two block shapes, why recovery is registry-driven, the supported CSS subset, and an explicit trust boundary note: recovered rich-text is emitted unescaped by the partials, so the hydrator is only safe over markup already trusted to render (theme files, patterns, editor-authored content) and is not a sanitizer. Wiki/API docs are worth a follow-up once a host wires renderMarkup() to a route.

Screenshots

n/a — server-side rendering change.

Pre-Submission Checklist

  • Followed contributing guidelines
  • Checked for other open PRs for same update
  • Code passes all tests
  • Code has been linted
  • Accessibility tests completed
  • Code follows project style guide
  • Self-review completed
  • Comments added for complex code
  • No new warnings generated

Reviewer notes

Security review: run over the branch — no findings at confidence ≥ 8. XXE was cleared (loadHTML uses libxml's HTML parser, which does not resolve external entities; LIBXML_NONET set, LIBXML_NOENT correctly not set). XPath injection cleared (selectors come only from block.json on disk; all values quoted, all names charset-constrained). The XSS trust boundary was traced and does not widen: the only wired path reads theme files on disk, already at PHP-execution trust.

Known gaps, deliberately out of scope:

  • core/html has no manifest or partial, so it hydrates with its innerHTML preserved but still renders via the unknown-block fallback. Worth a follow-up issue.
  • artisanpack/marquee declares sourced attributes but is absent from the service provider's registration lists, so they are never recovered — pre-existing, unrelated to this change.
  • EntitySearchController:93-100 has the same leading-backslash container-key bug fixed here in TemplatePartInliner; left alone to keep this diff scoped.

Pre-existing CI note: resources/js/visual-editor/site-editor/__tests__/site-editor-app.test.tsx has 18 failing tests. Verified these fail identically on a clean release/1.5.5 with this work stashed; this branch touches zero .tsx files.

Summary by CodeRabbit

  • New Features

    • Added support for rendering raw WordPress block markup, including nested blocks, theme templates, and synced patterns.
    • Improved recovery of block attributes from saved HTML, including rich text, images, lists, buttons, and nested content.
    • Added support for resolving template parts directly from raw theme markup.
    • Invalid or empty markup now safely returns no rendered output.
  • Bug Fixes

    • Improved handling of block namespaces, unresolved template parts, and malformed markup.

Block themes ship their templates as WP block markup, but nothing on
the server bridged cms-framework's parse_blocks() shape
({blockName, attrs, innerHTML}) to the shape BlockRenderer consumes
({name, attributes}). A key rename alone was not enough: Gutenberg
persists most block text in the SAVED HTML rather than the delimiter
JSON — zero of the artisanpack-ui theme's 130 paragraph/heading blocks
serialize a `content` attribute — so renaming keys produced a
structurally correct but completely textless page.

Adds BlockMarkupHydrator, which parses the markup AND replays each
registered block type's block.json attribute definitions back over its
saved HTML to recover content. Recovery is driven entirely by the
registry rather than a hand-maintained per-block table, so a block that
ships a new sourced attribute is picked up with no change here — which
is why this lives beside the block manifests and partials rather than in
cms-framework or a host app.

- BlockMarkupHydrator: hydrate(markup) and hydrateTree(parsedTree),
  with a depth cap and a documented trust boundary
- BlockAttributeSourceResolver: server-side port of Gutenberg's
  attribute/html/rich-text/text/tag/query/raw matchers over DOM + XPath
- CssSelectorToXPath: hand-rolled translator for the CSS subset the
  block manifests use, avoiding a new Composer dependency
- BlockRenderer::renderMarkup(): markup in, HTML out, with
  template-part and pattern inlining so a standalone theme template
  renders its parts as real content instead of empty wrappers
- TemplatePartInliner: hydrate a part's raw markup when it resolves
  with empty blocks, which is the .html-sourced theme-file case

Also drops the leading backslash from TemplatePartInliner's
RESOLVER_CLASS: with it, app() treated the string as a distinct
container key and silently ignored any host- or test-supplied override.

Closes #688
@coderabbitai

coderabbitai Bot commented Aug 2, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ViewFromTheBox, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 48 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: bfd66a04-b055-45df-859e-168797abc542

📥 Commits

Reviewing files that changed from the base of the PR and between 33c9273 and 7c5aada.

📒 Files selected for processing (3)
  • packages/visual-editor-renderer-blade/tests/Feature/RenderMarkupTest.php
  • tests/Unit/VisualEditor/Resources/TemplatePartInlinerRawContentTest.php
  • tests/Unit/VisualEditor/Support/BlockMarkupHydratorTest.php
📝 Walkthrough

Walkthrough

This change adds a server-side path from WordPress block markup to rendered HTML. It recovers block attributes from saved HTML, hydrates nested trees, inlines raw template parts and patterns, and exposes the flow through BlockRenderer::renderMarkup().

Changes

Markup rendering

Layer / File(s) Summary
Selector and attribute recovery
src/Support/CssSelectorToXPath.php, src/Support/BlockAttributeSourceResolver.php, src/Support/UnsupportedSelectorException.php, tests/Unit/VisualEditor/Support/CssSelectorToXPathTest.php
Adds CSS-to-XPath translation and DOM-based recovery for Gutenberg attribute sources, including scalar values, nested queries, defaults, type coercion, and malformed-selector handling.
Block tree hydration
src/Support/BlockMarkupHydrator.php, tests/Unit/VisualEditor/Support/BlockMarkupHydratorTest.php
Converts raw or parsed WordPress blocks into editor-shaped trees, recovers saved attributes, preserves saved HTML, maps namespaces, hydrates nested blocks, and limits recursion depth.
Renderer and template-part integration
packages/visual-editor-renderer-blade/src/BlockRenderer.php, src/VisualEditorServiceProvider.php, src/Resources/TemplatePartInliner.php, packages/visual-editor-renderer-blade/tests/Feature/RenderMarkupTest.php, tests/Unit/VisualEditor/Resources/TemplatePartInlinerRawContentTest.php
Adds renderMarkup(), registers the hydrator, and hydrates raw template-part markup when parsed blocks are unavailable. Tests cover rendering, namespace handling, nested parts, blank input, and parsed-block precedence.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant BlockRenderer
  participant BlockMarkupHydrator
  participant TemplatePartInliner
  participant BladeRenderer
  Caller->>BlockRenderer: renderMarkup(markup)
  BlockRenderer->>BlockMarkupHydrator: hydrate(markup)
  BlockMarkupHydrator-->>BlockRenderer: hydrated block tree
  BlockRenderer->>TemplatePartInliner: inline template parts
  TemplatePartInliner-->>BlockRenderer: resolved block tree
  BlockRenderer->>BladeRenderer: render(tree)
  BladeRenderer-->>Caller: rendered HTML
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: adding server-side rendering from WordPress block markup to HTML.
Linked Issues check ✅ Passed The changes satisfy issue #688 by hydrating block markup, recovering saved attributes, inlining template parts, and rendering the resulting tree.
Out of Scope Changes check ✅ Passed The new hydrator, attribute resolver, selector translator, renderer path, and tests directly support the requirements in issue #688.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bugfix/688-server-side-wp-block-markup-to-html

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ViewFromTheBox ViewFromTheBox moved this from Backlog to In Review in ArtisanPack UI Overview Aug 2, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Support/BlockMarkupHydrator.php`:
- Around line 259-299: Update hydrateBlock() to normalize core/* block names to
their registered artisanpack/* alias after attribute recovery, reusing
aliasFor() and preserving names already registered under artisanpack/*. Ensure
the normalized name is used for partial resolution and block rendering so
recovered attributes and content-only core blocks match the expected theme
partials.

In `@tests/Unit/VisualEditor/Resources/TemplatePartInlinerRawContentTest.php`:
- Around line 92-100: Rename the test case around TemplatePartInliner so its
description reflects the existing assertion that blank raw markup produces empty
innerBlocks, rather than claiming the part is marked unresolved. Keep the test
setup and assertions unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4bf3ed73-5a9d-437b-8095-3966c16a088e

📥 Commits

Reviewing files that changed from the base of the PR and between dafb6f9 and 33c9273.

📒 Files selected for processing (11)
  • packages/visual-editor-renderer-blade/src/BlockRenderer.php
  • packages/visual-editor-renderer-blade/tests/Feature/RenderMarkupTest.php
  • src/Resources/TemplatePartInliner.php
  • src/Support/BlockAttributeSourceResolver.php
  • src/Support/BlockMarkupHydrator.php
  • src/Support/CssSelectorToXPath.php
  • src/Support/UnsupportedSelectorException.php
  • src/VisualEditorServiceProvider.php
  • tests/Unit/VisualEditor/Resources/TemplatePartInlinerRawContentTest.php
  • tests/Unit/VisualEditor/Support/BlockMarkupHydratorTest.php
  • tests/Unit/VisualEditor/Support/CssSelectorToXPathTest.php

Comment on lines +259 to +299
protected function recoverAttributes( string $name, string $innerHtml ): array
{
if ( '' === trim( $innerHtml ) ) {
return [];
}

$definition = $this->blockTypes->get( $name );

if ( null === $definition ) {
$alias = $this->aliasFor( $name );

$definition = null !== $alias ? $this->blockTypes->get( $alias ) : null;
}

$attributes = $definition['attributes'] ?? null;

if ( ! is_array( $attributes ) || [] === $attributes ) {
return [];
}

return $this->sources->recover( $attributes, $innerHtml );
}

/**
* Map `core/x` ↔ `artisanpack/x`, or null when the name is in
* neither namespace.
*
* @since 1.5.5
*/
protected function aliasFor( string $name ): ?string
{
if ( str_starts_with( $name, 'core/' ) ) {
return 'artisanpack/' . substr( $name, strlen( 'core/' ) );
}

if ( str_starts_with( $name, 'artisanpack/' ) ) {
return 'core/' . substr( $name, strlen( 'artisanpack/' ) );
}

return null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Compare available block partials for the core and artisanpack namespaces.
set -euo pipefail

fd -t d 'blocks' packages/visual-editor-renderer-blade/resources/views

fd -t f -e blade.php . packages/visual-editor-renderer-blade/resources/views | sort

rg -n --glob '*.blade.php' -l 'wp-block-paragraph' packages/visual-editor-renderer-blade/resources/views

Repository: ArtisanPack-UI/visual-editor

Length of output: 14842


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Block partial namespace counts =="
find packages/visual-editor-renderer-blade/resources/views/blocks -maxdepth 2 -type f -name '*.blade.php' | sort | awk -F/ '{n=$6; c[n]++} END {for (x in c) print x ": " c[x]}' | sort

echo
echo "== Block type declaration references in block-types / definition files =="
git ls-files | rg '(^|/)(block-types|.*BlockTypes.*|.*Blocks.*\.(php|json|js|ts)|.*yaml$|.*yml$)' | head -200
rg -n "artisanpack/|core/|paragraph|audio|avatar|navigation" --glob '*.{php,json,js,ts,yaml,yml}' .

echo
echo "== Partial missing-core vs artisanpack comparison =="
python3 - <<'PY'
from pathlib import Path
import re
root = Path('packages/visual-editor-renderer-blade/resources/views/blocks')
namespaces = sorted({p/'' for ns,p in {part.parents[1]: ns for ns,part}})
for root, _dirs, files in Path('packages/visual-editor-renderer-blade/resources/views/blocks').walk():
    pass
PY

python3 - <<'PY'
from pathlib import Path
p = Path('packages/visual-editor-renderer-blade/resources/views/blocks')
namespaces = sorted({d.name for d in p.iterdir() if d.is_dir()})
print(namespaces)
for ns in namespaces:
    files = sorted(f.name.split('.')[0] for f in (p/ns).glob('*.blade.php'))
    print(ns, len(files), files[:10], '...', file=__import__('sys').stderr)
PY

Repository: ArtisanPack-UI/visual-editor

Length of output: 50385


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Block partial namespace counts =="
find packages/visual-editor-renderer-blade/resources/views/blocks/*/blocks -maxdepth 1 -type f -name '*.blade.php' 2>/dev/null || true
find packages/visual-editor-renderer-blade/resources/views/blocks -maxdepth 1 -type f -name '*.blade.php' | sort | awk -F/ '{ns=$6; c[ns]++} END {for (x in c) print x ": " c[x]}' | sort

echo
echo "== Files parsed by PHP that include a core block partial =="
python3 - <<'PY'
import ast, pathlib
root = pathlib.Path('packages/visual-editor-renderer-blade/resources/views/blocks')
patterns = []
for f in root.rglob('*.php'):
    text = f.read_text(errors='ignore')
    if '`@include`(' in text:
        patterns.append((f, text))
count = 0
for f, text in patterns:
    if 'blocks.core.' in text or '`@include`' in text:
        count += 1
        print(f)
print("includes_with_core_partial_or_include=", count)
for f, text in patterns[:5]:
    print("---", f)
    for i, line in enumerate(text.splitlines(), 1):
        if '`@include`(' in line or 'Delegates' in line:
            print(i, line)
PY

echo
echo "== Hydrator method outline and hydrateBlock section =="
ast-grep outline src/Support/BlockMarkupHydrator.php --view compact || true
sed -n '1,330p' src/Support/BlockMarkupHydrator.php

Repository: ArtisanPack-UI/visual-editor

Length of output: 16423


Normalize the hydrated block name back to the registered alias.

recoverAttributes() loads artisanpack/* attribute definitions for core/* nodes, but hydrateBlock() keeps the original core/* name. Block renders still depend on artisanpack/* partials and core content-only blocks would not match a blocks.core.* partial, so core-namespaced theme markup can render into an unknown-block wrapper and lose recovered attributes/content.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Support/BlockMarkupHydrator.php` around lines 259 - 299, Update
hydrateBlock() to normalize core/* block names to their registered artisanpack/*
alias after attribute recovery, reusing aliasFor() and preserving names already
registered under artisanpack/*. Ensure the normalized name is used for partial
resolution and block rendering so recovered attributes and content-only core
blocks match the expected theme partials.

Comment on lines +92 to +100
it( 'marks the part unresolved when the raw markup hydrates to nothing', function () {
bindPartResolver( themeFilePart( ' ' ) );

$tree = ( new TemplatePartInliner() )->inline( [
[ 'name' => 'core/template-part', 'attributes' => [ 'slug' => 'header' ], 'innerBlocks' => [] ],
] );

expect( $tree[0]['innerBlocks'] )->toBe( [] );
} );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Rename the test to match what it asserts.

The test name says the part is marked unresolved. The assertion only checks that innerBlocks is empty. findPartBlocks() returns the empty $blocks array on line 242 of src/Resources/TemplatePartInliner.php, not null, so resolvePart() never stamps _resolutionError for blank raw markup. The current name documents a guarantee the code does not provide.

Rename the test, or add the _resolutionError assertion and change findPartBlocks() to return null when the raw markup hydrates to nothing.

💚 Proposed rename
-it( 'marks the part unresolved when the raw markup hydrates to nothing', function () {
+it( 'inlines no children when the raw markup hydrates to nothing', function () {
 	bindPartResolver( themeFilePart( '   ' ) );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it( 'marks the part unresolved when the raw markup hydrates to nothing', function () {
bindPartResolver( themeFilePart( ' ' ) );
$tree = ( new TemplatePartInliner() )->inline( [
[ 'name' => 'core/template-part', 'attributes' => [ 'slug' => 'header' ], 'innerBlocks' => [] ],
] );
expect( $tree[0]['innerBlocks'] )->toBe( [] );
} );
it( 'inlines no children when the raw markup hydrates to nothing', function () {
bindPartResolver( themeFilePart( ' ' ) );
$tree = ( new TemplatePartInliner() )->inline( [
[ 'name' => 'core/template-part', 'attributes' => [ 'slug' => 'header' ], 'innerBlocks' => [] ],
] );
expect( $tree[0]['innerBlocks'] )->toBe( [] );
} );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Unit/VisualEditor/Resources/TemplatePartInlinerRawContentTest.php`
around lines 92 - 100, Rename the test case around TemplatePartInliner so its
description reflects the existing assertion that blank raw markup produces empty
innerBlocks, rather than claiming the part is marked unresolved. Keep the test
setup and assertions unchanged.

The PHP 8.2 CI leg installs cms-framework from Packagist, and 2.5+ —
which introduced BlockMarkupParser — requires PHP 8.3+. So on 8.2 the
parser is absent, hydrate() correctly degrades to an empty tree by
design, and the 23 tests that assert real recovery failed.

Gates exactly those 23 behind BlockMarkupHydrator::canParseMarkup(),
matching the existing `templatePartParserAvailable()` convention in
TemplateAdapterTest. Tests that exercise hydrateTree() directly, the
CSS->XPath translator, and the blank-input paths stay unskipped, since
none of them need the parser and all of them work on 8.2.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

1 participant