Skip to content

feat: M9 · Blade renderer package - #336

Merged
ViewFromTheBox merged 2 commits into
release/1.0from
feature/319-m9-blade-renderer-package
Apr 20, 2026
Merged

ViewFromTheBox merged 2 commits into
release/1.0from
feature/319-m9-blade-renderer-package

Conversation

@ViewFromTheBox

@ViewFromTheBox ViewFromTheBox commented Apr 20, 2026 •

Copy link
Copy Markdown
Member

Description

Ships the new artisanpack-ui/visual-editor-renderer-blade sub-package — a server-side Blade renderer that turns a saved visual-editor block tree into HTML using per-block Blade partials. Dynamic blocks invoke their registered DynamicBlock::render() inline with no extra HTTP round trip.

The sub-package lives inside this monorepo under packages/visual-editor-renderer-blade/ with its own standalone composer.json. Packagist distribution is documented in the new PACKAGING.md at the repo root — the subtree-split wiring itself is deferred to M15 (Docs, website, release), same approach Laravel Framework uses for illuminate/*.

Closes: #319

Type of Change

  • New feature (adds new functionality)

Related Issue

Issue: #319 (part of umbrella #309, V1 Gutenberg adoption)

Motivation and Context

M9 is the V1 frontend-renderer priority. Blade is the default Laravel frontend; this unlocks real public-site rendering of content authored in the M3 editor. Developers can now do:

<article>
    <x-ve-blocks :tree="$post->content" />
</article>

…and every block in the frozen V1 allow-list renders as the expected semantic HTML, while dynamic blocks (e.g. LatestPostsBlock) transparently call their PHP render() method.

Changes Made

  • New sub-package artisanpack-ui/visual-editor-renderer-blade at packages/visual-editor-renderer-blade/:
    • src/BlockRenderer.php — recursive tree walker; resolves partials, coerces views/HtmlString/scalars, catches render errors into the unknown-block fallback.
    • src/View/Components/BlocksComponent.php + resources/views/components/blocks.blade.php — the <x-ve-blocks> component; accepts arrays, JSON strings, or any Arrayable.
    • src/VisualEditorRendererBladeServiceProvider.php — auto-discovered provider; registers the component and publishes views under the visual-editor-blade-views tag.
    • 28 Blade partials under resources/views/blocks/core/ covering every block in the frozen V1 allow-list plus sub-blocks (core/column, core/button, core/list-item). core/latest-posts is dynamic-only so no static partial ships.
    • Own README.md with usage + override instructions.
  • Monorepo wiring:
    • Root composer.json autoloads the sub-package's src/ + tests/.
    • phpunit.xml gains a RendererBlade testsuite.
    • tests/Pest.php binds the sub-package's TestCase to its own test paths.
  • New PACKAGING.md at the repo root documenting the splitsh-based Packagist distribution plan for M15.
  • Dev-app demo (~/Herd/artisanpack-ui): new GET /packages/visual-editor/m9/post/{post} route that renders a Post's saved tree via <x-ve-blocks>, linked from the M3 editor + M3 index.

How Has This Been Tested?

Testing Environment:

  • Operating System: macOS (darwin 25.3)
  • Browser: Safari/Chrome on Laravel Herd
  • PHP Version: 8.4.3
  • Project Version: release/1.0 integration branch

Tests Performed:

  1. Authored a new M3 post in the dev app with mixed content (heading, paragraph, list with items, image).
  2. Hit the new /packages/visual-editor/m9/post/{id} route and verified the saved tree rendered as expected semantic HTML.
  3. Confirmed the empty-content case shows the "no content yet" notice and an unknown block renders the commented fallback <div> without breaking layout.
  4. Ran the full Pest suite (./vendor/bin/pest) — 126/126 passing, including the 28 new renderer-blade tests.

Accessibility Tests Run

  • Keyboard navigation tested — the rendered output is plain semantic HTML, tabs reach all links/form controls in preview view.
  • Screen reader tested — not in this pass; partial markup uses standard landmarks (<figure>, <blockquote>, <details>, etc.) rather than divs where possible.
  • Color contrast verified — no bespoke colors introduced.
  • ARIA labels checked — aria-hidden set on the cover block's overlay + spacer; role="search" on the search form.

Tests Added

  • Unit tests added (tests/Unit/BlockRendererTest.php — 24 tests covering every partial category, dynamic block delegation, validation hooks, view coercion, fallback escaping).
  • Integration tests added (tests/Feature/BlocksComponentTest.php — 4 tests covering the component from array/JSON trees, null tree, and the visual-editor-blade-views publish tag).
  • All tests passing (126 total).

Test details: Renderer tests use snapshot-style normalizeHtml() matching with insignificant whitespace collapsed between tags only, so partial edits don't break tests but structural changes do.

Documentation

  • Inline code documentation added — every class, method, and partial has PHPDoc headers following the package convention.
  • README updated — the sub-package ships its own README.md.
  • Wiki updated — deferred to M15 along with the rest of the V1 docs push.
  • API documentation updated — not applicable, no new HTTP endpoints.

Documentation details: PACKAGING.md at the repo root spells out the splitsh / Packagist distribution plan so M15 has a ready checklist.

Screenshots

Manually verified in-browser; the rendered output matches the block structure edited in M3.

Pre-Submission Checklist

  • Followed contributing guidelines
  • Checked for other open PRs for same update
  • Code passes all tests
  • Code has been linted — the project lint tooling (php-cs-fixer / Pint) isn't installed on the current dev env; code follows existing package conventions (real tabs, WordPress-style spacing, Yoda conditions, strict types).
  • Accessibility tests completed (keyboard navigation, semantic markup)
  • Code follows project style guide
  • Self-review completed
  • Comments added for complex code (renderer control flow, attribute normalization, view resolution)
  • No new warnings generated

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a Laravel package that server-renders Visual Editor block trees to HTML with many built-in block templates and a Blade component for easy embedding.
    • Includes safe URL handling and dynamic-block support with graceful fallbacks.
  • Documentation

    • Added comprehensive packaging and distribution guidance plus a README describing usage, view publishing, and supported block inputs.
  • Tests

    • New unit and feature tests covering rendering, sanitization, dynamic blocks, and edge-case safety.
  • Chores

    • Updated package manifest/autoload and test configuration for the new package.

Ship the new `artisanpack-ui/visual-editor-renderer-blade` sub-package that
renders saved visual-editor block trees to HTML server-side. Static blocks
render from per-block Blade partials; dynamic blocks invoke their registered
`DynamicBlock::render()` inline. Unknown blocks fall back to a commented
`<div>` wrapper so layout survives.

Adds:
- `src/BlockRenderer.php` — recursive tree walker that resolves partials via
  `visual-editor-renderer-blade::blocks.{namespace}.{block}`, coerces views /
  HtmlStrings / scalars to strings, and catches render errors into the
  unknown-block fallback.
- `<x-ve-blocks :tree="...">` component accepting arrays, JSON strings, or
  any Arrayable.
- 28 partials covering the frozen V1 allow-list + sub-blocks (`core/column`,
  `core/button`, `core/list-item`). `core/latest-posts` intentionally skipped
  since it needs dynamic data.
- `visual-editor-blade-views` publish tag so developers override individual
  block markup under `resources/views/vendor/visual-editor-renderer-blade/`.
- 28 Pest tests (unit + feature) covering happy paths, validation, dynamic
  block delegation, unknown-block fallback, XSS safety of the fallback
  marker, and the publish command.

Monorepo wiring:
- Root `composer.json` autoloads the sub-package's src/ + tests/.
- `phpunit.xml` gains a `RendererBlade` testsuite.
- `tests/Pest.php` binds the sub-package's TestCase to its own test paths.
- New `PACKAGING.md` at the repo root documents the splitsh-based Packagist
  distribution plan for M15.

Dev-app demo (at `~/Herd/artisanpack-ui`):
- Registers the new package via path repository.
- Adds `GET /packages/visual-editor/m9/post/{post}` route that renders a
  saved post via `<x-ve-blocks :tree="$post->content" />`.
- Adds "View public preview (M9)" link from the M3 editor + M3 index.

Closes #319

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 20, 2026 •

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 60af9ad8-96f4-463d-8dfc-550d49ee9f4f

📥 Commits

Reviewing files that changed from the base of the PR and between d0545a6 and 50e9c01.

📒 Files selected for processing (12)
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/button.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/buttons.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/column.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/cover.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/file.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/image.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/media-text.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/search.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/table.blade.php
  • packages/visual-editor-renderer-blade/src/BlockRenderer.php
  • packages/visual-editor-renderer-blade/src/Support/UrlSanitizer.php
  • packages/visual-editor-renderer-blade/tests/Unit/BlockRendererTest.php
✅ Files skipped from review due to trivial changes (1)
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/buttons.blade.php
🚧 Files skipped from review as they are similar to previous changes (8)
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/media-text.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/search.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/button.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/file.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/image.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/column.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/table.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/cover.blade.php

📝 Walkthrough

Walkthrough

A new Laravel package, visual-editor-renderer-blade, was added to the monorepo to server-side render Visual Editor block trees into HTML via Blade partials. It includes a BlockRenderer, a Blade component (), a service provider, 28+ core block templates, tests, and packaging/publishing documentation.

Changes

Cohort / File(s) Summary
Packaging & Docs
PACKAGING.md
Added packaging guide describing subtree-split mirrors, splitsh/lite workflow, CI push/tag replay plan, composer path interim install, and V1 release checklist.
Root Config & Test Integration
composer.json, phpunit.xml, tests/Pest.php
Registered PSR-4 autoload for the renderer package; added PHPUnit testsuite for RendererBlade; extended Pest to run the package's tests with its TestCase.
Package Manifest & README
packages/visual-editor-renderer-blade/composer.json, packages/visual-editor-renderer-blade/README.md
New package manifest (PHP ^8.2, Laravel 10–12, deps/dev-deps, provider discovery) and README documenting usage, accepted tree shapes, Blade component, view publishing, and included block partials.
Blade Templates (core blocks)
packages/visual-editor-renderer-blade/resources/views/blocks/core/*
Added 28+ core block Blade templates (paragraph, heading, image, list, table, gallery, cover, columns, media-text, buttons, quote, code, separator, spacer, video, audio, etc.) with attribute extraction, sanitization, class composition, conditional rendering, and caption handling.
Blade Component View
packages/visual-editor-renderer-blade/resources/views/components/blocks.blade.php
Simple component view that outputs the rendered HTML unescaped.
Renderer & Service Provider
packages/visual-editor-renderer-blade/src/BlockRenderer.php, .../View/Components/BlocksComponent.php, .../VisualEditorRendererBladeServiceProvider.php
Added BlockRenderer that resolves dynamic blocks (via DynamicBlockRegistry) or Blade partials, handles recursion, fallbacks, and stringification; BlocksComponent normalizes tree input and delegates rendering; service provider registers renderer, component, views, and publishable resources.
Support Utility
packages/visual-editor-renderer-blade/src/Support/UrlSanitizer.php
Added URL sanitizer utility whitelisting safe schemes and stripping disallowed-schemed URLs.
Tests & Test Infrastructure
packages/visual-editor-renderer-blade/tests/*, phpunit.xml
Added TestCase (Orchestra Testbench wiring), extensive unit tests for BlockRenderer covering many block types, sanitization, dynamic block behavior, and feature tests for the Blade component; added phpunit.xml testsuite entry.

Sequence Diagram

sequenceDiagram
    participant Template as "Blade Template"
    participant Component as "BlocksComponent"
    participant Renderer as "BlockRenderer"
    participant Registry as "DynamicBlockRegistry"
    participant View as "View Factory"
    participant Fallback as "Fallback Handler"

    Template->>Component: pass :tree prop (array | JSON | Arrayable)
    activate Component
    Component->>Component: normalizeTree(tree)
    Component->>Renderer: render(normalized_tree)
    deactivate Component

    activate Renderer
    loop each block
        Renderer->>Renderer: renderBlock(block)
        Renderer->>Renderer: extract name, attrs, innerBlocks
        Renderer->>Renderer: recursively render(innerBlocks)

        alt dynamic block registered
            Renderer->>Registry: get(block_name)
            Registry-->>Renderer: DynamicBlock instance
            Renderer->>Registry: validateAttrs(attrs)
            Registry-->>Renderer: validated_attrs
            Renderer->>Registry: render(validated_attrs)
            Registry-->>Renderer: result (View/Html/string)
            Renderer->>Renderer: coerce result to string
        else static block partial
            Renderer->>View: exists(partial_path)
            View-->>Renderer: true/false
            alt partial exists
                Renderer->>View: render(partial, data)
                View-->>Renderer: HTML
            else no partial
                Renderer->>Fallback: renderFallback(name, childrenHtml)
                Fallback-->>Renderer: fallback HTML
            end
        end
    end
    Renderer-->>Component: concatenated HTML
    deactivate Renderer

    Component-->>Template: provide html variable
    Template->>Template: output {!! html !!}
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 A Rabbit's Render Song
With blades and blocks I hop and write,
Trees to HTML by server light,
Partials hum and dynamic friends sing,
Pages blossom — hop, render, spring!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 48.39% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: M9 · Blade renderer package' directly and clearly describes the main addition: a new Blade renderer package for the visual-editor monorepo as part of the M9 milestone.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 feature/319-m9-blade-renderer-package

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

@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: 17

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (4)
PACKAGING.md-59-64 (1)

59-64: ⚠️ Potential issue | 🟡 Minor

Replace the machine-specific path example with a portable placeholder.

Line 62 hardcodes a local Desktop path, which is not reproducible for other contributors and will go stale quickly.

✍️ Suggested doc fix
 {
     "type": "path",
-    "url": "../../Desktop/ArtisanPack UI Packages/visual-editor/packages/visual-editor-renderer-blade",
+    "url": "../path-to-monorepo/packages/visual-editor-renderer-blade",
     "options": { "symlink": true }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@PACKAGING.md` around lines 59 - 64, Replace the machine-specific "url" value
in the JSON example with a portable placeholder (e.g.,
"../../path/to/visual-editor-renderer-blade" or "<LOCAL_PACKAGE_PATH>") so
contributors can substitute their own path; update the JSON block containing the
"type":"path" and "url" fields to use that placeholder and add a brief
comment/note explaining that the URL should be replaced with the contributor's
local package path.
packages/visual-editor-renderer-blade/resources/views/blocks/core/buttons.blade.php-2-6 (1)

2-6: ⚠️ Potential issue | 🟡 Minor

Avoid emitting conflicting justification classes.

Line 2 hardcodes is-content-justification-left, and Line 4-Line 6 may add a second justification class. This can produce conflicting layout classes in the same element.

💡 Proposed fix
-	$classes = [ 'wp-block-buttons', 'is-layout-flex', 'is-content-justification-left' ];
+	$classes = [ 'wp-block-buttons', 'is-layout-flex' ];
+	$justify = ! empty( $attributes['layout']['justifyContent'] )
+		? (string) $attributes['layout']['justifyContent']
+		: 'left';
+	$classes[] = 'is-content-justification-' . $justify;
-
-	if ( ! empty( $attributes['layout']['justifyContent'] ) ) {
-		$classes[] = 'is-content-justification-' . $attributes['layout']['justifyContent'];
-	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/visual-editor-renderer-blade/resources/views/blocks/core/buttons.blade.php`
around lines 2 - 6, The code currently seeds $classes with a hardcoded
'is-content-justification-left' and then may append another justification class
from $attributes['layout']['justifyContent', causing conflicting classes; update
the logic in blocks/core/buttons.blade.php so $classes does not include a
default justification up front, then check
$attributes['layout']['justifyContent'] and push
'is-content-justification-{value}' if present, otherwise push the default
'is-content-justification-left' — i.e., move the default-adding into the
conditional that handles justifyContent (referencing the $classes array and
$attributes['layout']['justifyContent'] symbols).
packages/visual-editor-renderer-blade/resources/views/blocks/core/column.blade.php-14-17 (1)

14-17: ⚠️ Potential issue | 🟡 Minor

Preserve fractional widths when computing flex-basis.

Line 16 truncates numeric widths to integers, so values like 33.33 render as 33%.

Proposed fix
-		$basis     = is_numeric( $width ) ? sprintf( '%d%%', (int) $width ) : (string) $width;
+		$basis     = is_numeric( $width ) ? sprintf( '%s%%', rtrim( rtrim( (string) $width, '0' ), '.' ) ) : (string) $width;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/visual-editor-renderer-blade/resources/views/blocks/core/column.blade.php`
around lines 14 - 17, The current numeric width handling in the column view
truncates fractional widths by casting $width to (int) when building $basis;
update the logic around $width / $basis (the is_numeric check and the sprintf
call) to preserve fractional values instead of integer-casting — convert $width
to a float or string representation that keeps decimals, trim any unnecessary
trailing zeros/dot, then append the '%' unit when building $basis and pass that
into the existing sprintf(' style="flex-basis: %s;"', e( $basis )) so values
like 33.33 remain 33.33% while still handling integer inputs cleanly.
packages/visual-editor-renderer-blade/tests/TestCase.php-51-55 (1)

51-55: ⚠️ Potential issue | 🟡 Minor

normalizeHtml() behavior contradicts its contract.

Current implementation collapses all whitespace, including text content, while doc says only inter-tag whitespace should be removed.

Proposed fix
 	protected function normalizeHtml( string $html ): string
 	{
 		$betweenTags = (string) preg_replace( '/>\s+</', '><', $html );
-
-		$attrCollapsed = (string) preg_replace( '/\s+/', ' ', $betweenTags );
-
-		return trim( $attrCollapsed );
+		return trim( $betweenTags );
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/visual-editor-renderer-blade/tests/TestCase.php` around lines 51 -
55, normalizeHtml currently collapses all whitespace by applying
preg_replace('/\s+/',' ') to $betweenTags, which alters text node content
contrary to its contract; update the implementation so only whitespace inside
tags/attributes is collapsed: operate on tag substrings only (e.g., run the
attribute-collapsing replace on each match of a tag via a tag-only transform)
and leave text node whitespace untouched—adjust the code that produces
$attrCollapsed so it processes tag matches from $betweenTags rather than
applying a global '/\s+/' replace. Ensure references to normalizeHtml,
$betweenTags and $attrCollapsed are preserved.
🧹 Nitpick comments (4)
packages/visual-editor-renderer-blade/resources/views/blocks/core/spacer.blade.php (1)

2-8: Constrain spacer height to valid CSS length formats before building inline style.

Line 2–Line 8 currently allow arbitrary strings (including additional CSS declarations via ;). A small normalization/validation step would make this safer and more predictable.

🛠️ Suggested hardening
 `@php`
-	$height = $attributes['height'] ?? '100px';
+	$height = $attributes['height'] ?? '100px';

 	if ( is_numeric( $height ) ) {
 		$height = $height . 'px';
 	}
 
-	$style = sprintf( 'height: %s;', (string) $height );
+	$height = (string) $height;
+	if ( ! preg_match( '/^-?\d+(\.\d+)?(px|em|rem|vh|vw|%)$/', $height ) ) {
+		$height = '100px';
+	}
+
+	$style = sprintf( 'height: %s;', $height );
 `@endphp`
packages/visual-editor-renderer-blade/resources/views/components/blocks.blade.php (1)

1-1: Document the trust boundary for raw HTML output.

Line 1 intentionally renders unescaped HTML. Add an inline note (or matching README note) that $html must be trusted/sanitized upstream to prevent accidental unsafe reuse later.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/visual-editor-renderer-blade/resources/views/components/blocks.blade.php`
at line 1, Add documentation around the unescaped output: annotate the Blade
snippet that renders {!! $html !!} (and/or the component README) with a clear
trust boundary note stating that $html must be pre-sanitized or originates from
a trusted source and must never contain user-controlled content; update the
component comment or README near the rendering logic (the {!! $html !!} usage)
to describe who is responsible for sanitization and acceptable input types so
future contributors do not pass unsafe data into $html.
packages/visual-editor-renderer-blade/resources/views/blocks/core/columns.blade.php (1)

4-6: Make isStackedOnMobile handling explicit.

Line 4 mixes empty() and isset(), which makes falsey cases ambiguous. Prefer an explicit normalized boolean and a single condition for maintainability and predictability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/visual-editor-renderer-blade/resources/views/blocks/core/columns.blade.php`
around lines 4 - 6, The condition mixing empty() and isset() for
$attributes['isStackedOnMobile'] is ambiguous; normalize the value to an
explicit boolean (e.g. derive $isStackedOnMobile from
$attributes['isStackedOnMobile'] with a default true/false as intended) and
replace the combined empty/isset check with a single if ($isStackedOnMobile)
that appends 'is-stacked-on-mobile' to $classes; update references to
$attributes['isStackedOnMobile'] accordingly so the logic is clear and
predictable.
packages/visual-editor-renderer-blade/tests/Feature/BlocksComponentTest.php (1)

45-50: vendor:publish --force can make this test stateful across runs.

Line 48 mutates published view files globally. Consider isolating this (temporary app path or explicit cleanup) to reduce order-dependent failures in larger/parallel test runs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/visual-editor-renderer-blade/tests/Feature/BlocksComponentTest.php`
around lines 45 - 50, The test it('publishes block views under the
visual-editor-blade-views tag') mutates global published view files by running
$this->artisan('vendor:publish', [..., '--force' => true]) which can make runs
stateful; to fix, isolate side effects by redirecting view publishing into a
temporary path before invoking the command (e.g., create a temp directory,
replace config('view.paths') or swap the app view path to that temp dir, run the
$this->artisan('vendor:publish', ['--tag' => 'visual-editor-blade-views',
'--force' => true]), assert exit code, then remove the temp directory and
restore the original paths), referencing the test block and the artisan
vendor:publish call to locate where to apply the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/visual-editor-renderer-blade/resources/views/blocks/core/audio.blade.php`:
- Around line 34-35: The code currently outputs caption HTML raw via {!!
$caption !!} which trusts persisted HTML; change the figcaption rendering to
escape the caption (e.g., use Blade's escaped output {{ $caption }} or apply a
sanitization helper like esc_html() / e($caption)) so the $caption variable is
safely escaped before being rendered in the <figcaption> element.

In
`@packages/visual-editor-renderer-blade/resources/views/blocks/core/button.blade.php`:
- Around line 18-33: Validate and sanitize the href and enforce safe rel
behavior: ensure $url is checked for an allowed scheme (e.g., http, https,
mailto) before rendering and fallback to empty/escapes if invalid, and escape
the output (use an escaping helper equivalent to e() when outputting $url).
Also, when $target equals "_blank" ensure $rel contains at least "noopener
noreferrer" (append them if missing) before adding to $linkAttrs, and continue
to escape $target, $rel, and $title (the variables referenced: $url, $target,
$rel, $title, $linkAttrs, $linkClasses, $text) so the anchor is safe from
javascript/data URIs and tabnabbing.

In
`@packages/visual-editor-renderer-blade/resources/views/blocks/core/cover.blade.php`:
- Around line 3-24: The minHeightUnit and dimRatio need sanitization: whitelist
minHeightUnit to an explicit set (e.g., 'px','em','rem','vh','vw','%') and
fallback to 'px' if not allowed, clamp $dimRatio to an integer range 0–100 after
casting, and ensure $minHeight is cast to a positive float before use; then use
these sanitized values when building $styleAttr and any dim/opacity output
(referencing $minHeight, $minHeightUnit, $dimRatio, and $styleAttr) so only safe
CSS tokens and valid opacity values are emitted.

In
`@packages/visual-editor-renderer-blade/resources/views/blocks/core/details.blade.php`:
- Around line 2-15: The summary value is taken from untrusted $attributes and
rendered raw via {!! $summary !!}, enabling XSS; sanitize or escape $summary
before output by either sanitizing on assignment (e.g., set $summary =
wp_kses_post($attributes['summary'] ?? '') or another appropriate wp_kses
function) or switch to Blade's escaped output for the <summary> element (use the
escaped output helper instead of raw output). Update references to the $summary
variable and the <summary> tag in this template so the value is properly escaped
before rendering.

In
`@packages/visual-editor-renderer-blade/resources/views/blocks/core/file.blade.php`:
- Around line 2-7: The href-related variables ($href, $textLinkHref, $linkLabel)
are assigned directly from user-provided attributes which allows dangerous
schemes like javascript: or data: to be written into anchors; validate and
whitelist URL schemes before using them: parse the scheme for $href and
$textLinkHref (e.g. via parse_url(..., PHP_URL_SCHEME)) and only allow safe
schemes such as http, https, mailto, tel (or fallback to an empty string/'#'
when missing/unsafe), then use the validated/normalized values for $href,
$textLinkHref and $linkLabel so no untrusted scheme can be injected into the
output.

In
`@packages/visual-editor-renderer-blade/resources/views/blocks/core/gallery.blade.php`:
- Around line 20-21: The gallery caption currently outputs raw HTML via the {!!
$caption !!} echo in blocks/core/gallery.blade.php which allows injection from
persisted attributes; replace the raw echo with an escaped output (either use
Blade's escaped echo {{ $caption }} to strip all HTML, or if you need to allow
safe HTML use {!! wp_kses_post( $caption ) !!} instead) so the figcaption no
longer renders untrusted raw HTML.

In
`@packages/visual-editor-renderer-blade/resources/views/blocks/core/heading.blade.php`:
- Around line 16-18: The template renders untrusted $content raw via the {!!
$content !!} expression, creating an XSS risk; update the Heading block to
sanitize or escape $content before rendering (e.g., run $content through a
sanitizer like HTMLPurifier or use Blade's escaped output e($content) / {{ }})
and then output the sanitized value in place of the raw {!! $content !!}; locate
the usage of $attributes['content'], the $content variable, the {!! $content !!}
expression and the surrounding $tag / $classes to make the change.

In
`@packages/visual-editor-renderer-blade/resources/views/blocks/core/image.blade.php`:
- Around line 42-50: The output currently renders an <a> when $href is non-empty
and emits the caption raw; update the image block to only output the anchor when
$href has an allowed scheme (allow relative URLs and schemes http, https,
mailto, tel) by validating/parsing $href (e.g., check parse_url($href)['scheme']
or use a framework helper) and skip the <a> if the scheme is not in the
allowlist, and change the caption rendering to escape HTML instead of raw output
(replace {!! $caption !!} with an escaped/filtered output such as {{ $caption }}
or apply a safe sanitizer) so $caption cannot inject markup; keep $imgAttrs
usage unchanged but ensure validation happens before echoing $href and $caption.

In
`@packages/visual-editor-renderer-blade/resources/views/blocks/core/media-text.blade.php`:
- Around line 24-27: The template computes the wrong right-column width: in the
conditional that sets $columns based on $mediaPosition and $mediaWidth, change
the branch that currently uses sprintf('auto %d%%', 100 - $mediaWidth) to use
the requested media width instead (e.g. sprintf('auto %d%%', $mediaWidth)) so
that when $mediaPosition is not 'left' the media column width comes from
$mediaWidth; update the $columns assignment that references $mediaPosition,
$mediaWidth, and $columns accordingly.

In
`@packages/visual-editor-renderer-blade/resources/views/blocks/core/paragraph.blade.php`:
- Around line 12-14: The paragraph template renders raw HTML via {!! $content
!!} but BlockRenderer::normalizeAttributes() does not sanitize attributes, so
either enforce sanitization in the renderer or stop rendering raw HTML here;
update BlockRenderer::normalizeAttributes() to validate/sanitize the 'content'
attribute (e.g., run it through an HTML sanitizer or whitelist via HTMLPurifier)
and then render sanitized content in paragraph.blade.php, or change the template
to escape by default (use escaped output) and only allow raw output when a
documented, validated "trusted_html" flag is present in the attributes;
reference $content, normalizeAttributes(), and the paragraph template when
making the change.

In
`@packages/visual-editor-renderer-blade/resources/views/blocks/core/search.blade.php`:
- Around line 6-8: Change the button-use value to a boolean flag: set $buttonUse
= !empty($attributes['buttonUseIcon']); then compute the label with $buttonLabel
= $buttonUse ? '' : $buttonText so a boolean true no longer casts to "1" and
will not be used as the button text; update the code that reads
$attributes['buttonUseIcon'] (and keep $queryName assignment as-is) so rendering
uses the boolean flag ($buttonUse) and not a string-cast value.
- Around line 26-29: The template hardcodes the input id "wp-block-search-input"
and the label's for attribute, causing duplicate IDs; update the search block to
generate a unique id per instance (e.g., using an existing unique block
identifier or generating one like $blockId or uniqid()) and replace both the
label's for and the input's id to use that unique variable so the label-input
association and DOM IDs are unique (update the label element with class
wp-block-search__label and the input element with class wp-block-search__input
to reference the new unique id).

In
`@packages/visual-editor-renderer-blade/resources/views/blocks/core/table.blade.php`:
- Around line 32-44: The table renderer currently outputs raw HTML from block
attributes ($cell['content'] and $caption) which permits script injection;
update the $renderCell closure to HTML-escape cell content (use e($cellContent)
or htmlspecialchars with ENT_QUOTES|ENT_SUBSTITUTE) when building the cell
string instead of outputting $cellContent directly, and likewise ensure the
caption string is escaped before being injected into the caption tag (replace
any raw $caption usage with e($caption)); apply the same escaping change to the
other cell rendering branch referenced (the second cell loop around the same
rendering logic).
- Around line 34-41: $user-controlled $cellAlign is being interpolated into
$styleAttr for inline CSS; validate it against a strict allow-list before use
and fall back to empty string if invalid. Replace the current assignment of
$cellAlign (from $cell['align']) with a check against allowed tokens
['left','center','right','justify'] (case-normalize as needed) and only set
$styleAttr when the validated value is non-empty; keep using e($cellAlign) when
writing the attribute. Locate references to $cellAlign, $styleAttr and the
source $cell['align'] in the block to implement this validation and fallback.

In
`@packages/visual-editor-renderer-blade/resources/views/blocks/core/verse.blade.php`:
- Around line 2-10: The template currently outputs untrusted $content raw ({!!
$content !!}) which creates an XSS risk; update the rendering in the verse block
to sanitize/escape the content before output (use WordPress sanitizers such as
wp_kses_post() with an appropriate allowed-tags list or esc_html() if HTML
should be stripped) and ensure the change is applied where $content is prepared
and where the template prints it (references: the $content variable and the
final pre element/classes logic that currently uses {!! $content !!}); replace
the raw output with a sanitized/escaped variant so only allowed markup is
rendered.

In
`@packages/visual-editor-renderer-blade/resources/views/blocks/core/video.blade.php`:
- Around line 53-55: The caption is currently rendered raw via the {!! $caption
!!} expression which allows XSS; change the figcaption output to use Blade's
escaped output (e.g. use {{ $caption }} or {!! e($caption) !!}) so $caption is
escaped before rendering, keeping the existing trim($caption) check and the
surrounding `@if` block intact.

In `@packages/visual-editor-renderer-blade/src/BlockRenderer.php`:
- Around line 95-107: renderDynamic currently lets exceptions from
$this->dynamicBlocks->get, $block->validateAttrs, $block->render or view
rendering bubble up; update renderDynamic to wrap the dynamic block handling in
a try/catch that mirrors renderStatic's defensive behavior: catch Throwable
around the validation/render steps (and the case where $block is null is already
handled), log the error with the same context as renderStatic, and on any
failure return $this->renderStatic($name, $attributes, $innerBlocksHtml) after
coercing or returning its string result via $this->coerceToString; reference
renderDynamic, $this->dynamicBlocks, validateAttrs, render, coerceToString and
renderStatic when making the change.

---

Minor comments:
In
`@packages/visual-editor-renderer-blade/resources/views/blocks/core/buttons.blade.php`:
- Around line 2-6: The code currently seeds $classes with a hardcoded
'is-content-justification-left' and then may append another justification class
from $attributes['layout']['justifyContent', causing conflicting classes; update
the logic in blocks/core/buttons.blade.php so $classes does not include a
default justification up front, then check
$attributes['layout']['justifyContent'] and push
'is-content-justification-{value}' if present, otherwise push the default
'is-content-justification-left' — i.e., move the default-adding into the
conditional that handles justifyContent (referencing the $classes array and
$attributes['layout']['justifyContent'] symbols).

In
`@packages/visual-editor-renderer-blade/resources/views/blocks/core/column.blade.php`:
- Around line 14-17: The current numeric width handling in the column view
truncates fractional widths by casting $width to (int) when building $basis;
update the logic around $width / $basis (the is_numeric check and the sprintf
call) to preserve fractional values instead of integer-casting — convert $width
to a float or string representation that keeps decimals, trim any unnecessary
trailing zeros/dot, then append the '%' unit when building $basis and pass that
into the existing sprintf(' style="flex-basis: %s;"', e( $basis )) so values
like 33.33 remain 33.33% while still handling integer inputs cleanly.

In `@packages/visual-editor-renderer-blade/tests/TestCase.php`:
- Around line 51-55: normalizeHtml currently collapses all whitespace by
applying preg_replace('/\s+/',' ') to $betweenTags, which alters text node
content contrary to its contract; update the implementation so only whitespace
inside tags/attributes is collapsed: operate on tag substrings only (e.g., run
the attribute-collapsing replace on each match of a tag via a tag-only
transform) and leave text node whitespace untouched—adjust the code that
produces $attrCollapsed so it processes tag matches from $betweenTags rather
than applying a global '/\s+/' replace. Ensure references to normalizeHtml,
$betweenTags and $attrCollapsed are preserved.

In `@PACKAGING.md`:
- Around line 59-64: Replace the machine-specific "url" value in the JSON
example with a portable placeholder (e.g.,
"../../path/to/visual-editor-renderer-blade" or "<LOCAL_PACKAGE_PATH>") so
contributors can substitute their own path; update the JSON block containing the
"type":"path" and "url" fields to use that placeholder and add a brief
comment/note explaining that the URL should be replaced with the contributor's
local package path.

---

Nitpick comments:
In
`@packages/visual-editor-renderer-blade/resources/views/blocks/core/columns.blade.php`:
- Around line 4-6: The condition mixing empty() and isset() for
$attributes['isStackedOnMobile'] is ambiguous; normalize the value to an
explicit boolean (e.g. derive $isStackedOnMobile from
$attributes['isStackedOnMobile'] with a default true/false as intended) and
replace the combined empty/isset check with a single if ($isStackedOnMobile)
that appends 'is-stacked-on-mobile' to $classes; update references to
$attributes['isStackedOnMobile'] accordingly so the logic is clear and
predictable.

In
`@packages/visual-editor-renderer-blade/resources/views/components/blocks.blade.php`:
- Line 1: Add documentation around the unescaped output: annotate the Blade
snippet that renders {!! $html !!} (and/or the component README) with a clear
trust boundary note stating that $html must be pre-sanitized or originates from
a trusted source and must never contain user-controlled content; update the
component comment or README near the rendering logic (the {!! $html !!} usage)
to describe who is responsible for sanitization and acceptable input types so
future contributors do not pass unsafe data into $html.

In `@packages/visual-editor-renderer-blade/tests/Feature/BlocksComponentTest.php`:
- Around line 45-50: The test it('publishes block views under the
visual-editor-blade-views tag') mutates global published view files by running
$this->artisan('vendor:publish', [..., '--force' => true]) which can make runs
stateful; to fix, isolate side effects by redirecting view publishing into a
temporary path before invoking the command (e.g., create a temp directory,
replace config('view.paths') or swap the app view path to that temp dir, run the
$this->artisan('vendor:publish', ['--tag' => 'visual-editor-blade-views',
'--force' => true]), assert exit code, then remove the temp directory and
restore the original paths), referencing the test block and the artisan
vendor:publish call to locate where to apply the change.
🪄 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

Run ID: a4ac8419-fa87-43a0-b216-04a4aba17381

📥 Commits

Reviewing files that changed from the base of the PR and between 20096bd and d0545a6.

📒 Files selected for processing (42)
  • PACKAGING.md
  • composer.json
  • packages/visual-editor-renderer-blade/README.md
  • packages/visual-editor-renderer-blade/composer.json
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/audio.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/button.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/buttons.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/code.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/column.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/columns.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/cover.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/details.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/embed.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/file.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/gallery.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/group.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/heading.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/image.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/list-item.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/list.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/media-text.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/paragraph.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/preformatted.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/pullquote.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/quote.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/row.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/search.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/separator.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/spacer.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/stack.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/table.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/verse.blade.php
  • packages/visual-editor-renderer-blade/resources/views/blocks/core/video.blade.php
  • packages/visual-editor-renderer-blade/resources/views/components/blocks.blade.php
  • packages/visual-editor-renderer-blade/src/BlockRenderer.php
  • packages/visual-editor-renderer-blade/src/View/Components/BlocksComponent.php
  • packages/visual-editor-renderer-blade/src/VisualEditorRendererBladeServiceProvider.php
  • packages/visual-editor-renderer-blade/tests/Feature/BlocksComponentTest.php
  • packages/visual-editor-renderer-blade/tests/TestCase.php
  • packages/visual-editor-renderer-blade/tests/Unit/BlockRendererTest.php
  • phpunit.xml
  • tests/Pest.php

Comment on lines +34 to +35
@if ( '' !== trim( $caption ) )
<figcaption>{!! $caption !!}</figcaption>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Escape audio captions instead of raw rendering.

Line 35 should not trust persisted caption HTML directly.

Proposed fix
-		<figcaption>{!! $caption !!}</figcaption>
+		<figcaption>{{ $caption }}</figcaption>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/visual-editor-renderer-blade/resources/views/blocks/core/audio.blade.php`
around lines 34 - 35, The code currently outputs caption HTML raw via {!!
$caption !!} which trusts persisted HTML; change the figcaption rendering to
escape the caption (e.g., use Blade's escaped output {{ $caption }} or apply a
sanitization helper like esc_html() / e($caption)) so the $caption variable is
safely escaped before being rendered in the <figcaption> element.

Comment thread packages/visual-editor-renderer-blade/resources/views/blocks/core/cover.blade.php Outdated
Comment on lines +2 to +15
$summary = (string) ( $attributes['summary'] ?? '' );
$showContent = ! empty( $attributes['showContent'] );

$classes = [ 'wp-block-details' ];

if ( ! empty( $attributes['className'] ) ) {
$classes[] = $attributes['className'];
}

$openAttr = $showContent ? ' open' : '';
@endphp
<details class="{{ implode( ' ', array_map( 'trim', $classes ) ) }}"{!! $openAttr !!}>
<summary>{!! $summary !!}</summary>
{!! $innerBlocksHtml !!}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Escape/sanitize summary before rendering to prevent XSS.

Line 14 renders $summary with raw Blade output even though Line 2 sources it from untrusted attributes. This permits script injection.

🔒 Proposed fix
-	<summary>{!! $summary !!}</summary>
+	<summary>{{ $summary }}</summary>
📝 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
$summary = (string) ( $attributes['summary'] ?? '' );
$showContent = ! empty( $attributes['showContent'] );
$classes = [ 'wp-block-details' ];
if ( ! empty( $attributes['className'] ) ) {
$classes[] = $attributes['className'];
}
$openAttr = $showContent ? ' open' : '';
@endphp
<details class="{{ implode( ' ', array_map( 'trim', $classes ) ) }}"{!! $openAttr !!}>
<summary>{!! $summary !!}</summary>
{!! $innerBlocksHtml !!}
$summary = (string) ( $attributes['summary'] ?? '' );
$showContent = ! empty( $attributes['showContent'] );
$classes = [ 'wp-block-details' ];
if ( ! empty( $attributes['className'] ) ) {
$classes[] = $attributes['className'];
}
$openAttr = $showContent ? ' open' : '';
`@endphp`
<details class="{{ implode( ' ', array_map( 'trim', $classes ) ) }}"{!! $openAttr !!}>
<summary>{{ $summary }}</summary>
{!! $innerBlocksHtml !!}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/visual-editor-renderer-blade/resources/views/blocks/core/details.blade.php`
around lines 2 - 15, The summary value is taken from untrusted $attributes and
rendered raw via {!! $summary !!}, enabling XSS; sanitize or escape $summary
before output by either sanitizing on assignment (e.g., set $summary =
wp_kses_post($attributes['summary'] ?? '') or another appropriate wp_kses
function) or switch to Blade's escaped output for the <summary> element (use the
escaped output helper instead of raw output). Update references to the $summary
variable and the <summary> tag in this template so the value is properly escaped
before rendering.

Comment thread packages/visual-editor-renderer-blade/resources/views/blocks/core/file.blade.php Outdated
Comment on lines +32 to +44
$renderCell = static function ( array $cell, string $defaultTag ): string {
$cellContent = (string) ( $cell['content'] ?? '' );
$cellAlign = isset( $cell['align'] ) ? (string) $cell['align'] : '';
$tag = isset( $cell['tag'] ) && in_array( $cell['tag'], [ 'td', 'th' ], true )
? $cell['tag']
: $defaultTag;

$styleAttr = '' !== $cellAlign
? sprintf( ' style="text-align: %s;"', e( $cellAlign ) )
: '';

return sprintf( '<%1$s%2$s>%3$s</%1$s>', $tag, $styleAttr, $cellContent );
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Unescaped table content/caption allows script injection.

$cell['content'] and $caption are rendered as raw HTML from block attributes, which are not sanitized in the renderer pipeline.

Proposed fix
-		return sprintf( '<%1$s%2$s>%3$s</%1$s>', $tag, $styleAttr, $cellContent );
+		return sprintf( '<%1$s%2$s>%3$s</%1$s>', $tag, $styleAttr, e( $cellContent ) );
@@
-		<figcaption>{!! $caption !!}</figcaption>
+		<figcaption>{{ $caption }}</figcaption>

Also applies to: 65-67

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/visual-editor-renderer-blade/resources/views/blocks/core/table.blade.php`
around lines 32 - 44, The table renderer currently outputs raw HTML from block
attributes ($cell['content'] and $caption) which permits script injection;
update the $renderCell closure to HTML-escape cell content (use e($cellContent)
or htmlspecialchars with ENT_QUOTES|ENT_SUBSTITUTE) when building the cell
string instead of outputting $cellContent directly, and likewise ensure the
caption string is escaped before being injected into the caption tag (replace
any raw $caption usage with e($caption)); apply the same escaping change to the
other cell rendering branch referenced (the second cell loop around the same
rendering logic).

Comment on lines +34 to +41
$cellAlign = isset( $cell['align'] ) ? (string) $cell['align'] : '';
$tag = isset( $cell['tag'] ) && in_array( $cell['tag'], [ 'td', 'th' ], true )
? $cell['tag']
: $defaultTag;

$styleAttr = '' !== $cellAlign
? sprintf( ' style="text-align: %s;"', e( $cellAlign ) )
: '';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Constrain text-align to known tokens.

$cellAlign is user-controlled and inserted into inline CSS. Please allow-list values (left|center|right|justify) before use.

Proposed fix
 		$cellContent = (string) ( $cell['content'] ?? '' );
 		$cellAlign   = isset( $cell['align'] ) ? (string) $cell['align'] : '';
+		$cellAlign   = in_array( $cellAlign, [ 'left', 'center', 'right', 'justify' ], true ) ? $cellAlign : '';
 		$tag         = isset( $cell['tag'] ) && in_array( $cell['tag'], [ 'td', 'th' ], true )
 			? $cell['tag']
 			: $defaultTag;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/visual-editor-renderer-blade/resources/views/blocks/core/table.blade.php`
around lines 34 - 41, $user-controlled $cellAlign is being interpolated into
$styleAttr for inline CSS; validate it against a strict allow-list before use
and fall back to empty string if invalid. Replace the current assignment of
$cellAlign (from $cell['align']) with a check against allowed tokens
['left','center','right','justify'] (case-normalize as needed) and only set
$styleAttr when the validated value is non-empty; keep using e($cellAlign) when
writing the attribute. Locate references to $cellAlign, $styleAttr and the
source $cell['align'] in the block to implement this validation and fallback.

Comment on lines +2 to +10
$content = (string) ( $attributes['content'] ?? '' );

$classes = [ 'wp-block-verse' ];

if ( ! empty( $attributes['textAlign'] ) ) {
$classes[] = 'has-text-align-' . $attributes['textAlign'];
}
@endphp
<pre class="{{ implode( ' ', array_map( 'trim', $classes ) ) }}">{!! $content !!}</pre>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Raw verse content output is unsafe (XSS risk).

Line 10 renders untrusted $content as raw HTML. This needs escaping or explicit sanitization.

🔒 Proposed fix
-<pre class="{{ implode( ' ', array_map( 'trim', $classes ) ) }}">{!! $content !!}</pre>
+<pre class="{{ implode( ' ', array_map( 'trim', $classes ) ) }}">{{ $content }}</pre>
📝 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
$content = (string) ( $attributes['content'] ?? '' );
$classes = [ 'wp-block-verse' ];
if ( ! empty( $attributes['textAlign'] ) ) {
$classes[] = 'has-text-align-' . $attributes['textAlign'];
}
@endphp
<pre class="{{ implode( ' ', array_map( 'trim', $classes ) ) }}">{!! $content !!}</pre>
$content = (string) ( $attributes['content'] ?? '' );
$classes = [ 'wp-block-verse' ];
if ( ! empty( $attributes['textAlign'] ) ) {
$classes[] = 'has-text-align-' . $attributes['textAlign'];
}
`@endphp`
<pre class="{{ implode( ' ', array_map( 'trim', $classes ) ) }}">{{ $content }}</pre>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/visual-editor-renderer-blade/resources/views/blocks/core/verse.blade.php`
around lines 2 - 10, The template currently outputs untrusted $content raw ({!!
$content !!}) which creates an XSS risk; update the rendering in the verse block
to sanitize/escape the content before output (use WordPress sanitizers such as
wp_kses_post() with an appropriate allowed-tags list or esc_html() if HTML
should be stripped) and ensure the change is applied where $content is prepared
and where the template prints it (references: the $content variable and the
final pre element/classes logic that currently uses {!! $content !!}); replace
the raw output with a sanitized/escaped variant so only allowed markup is
rendered.

Comment on lines +53 to +55
@if ( '' !== trim( $caption ) )
<figcaption>{!! $caption !!}</figcaption>
@endif

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Escape caption output to prevent HTML/script injection.

Line 54 uses raw output for $caption. Since attributes are rendered directly from saved block data, this is an XSS risk.

Proposed fix
-		<figcaption>{!! $caption !!}</figcaption>
+		<figcaption>{{ $caption }}</figcaption>
📝 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
@if ( '' !== trim( $caption ) )
<figcaption>{!! $caption !!}</figcaption>
@endif
`@if` ( '' !== trim( $caption ) )
<figcaption>{{ $caption }}</figcaption>
`@endif`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/visual-editor-renderer-blade/resources/views/blocks/core/video.blade.php`
around lines 53 - 55, The caption is currently rendered raw via the {!! $caption
!!} expression which allows XSS; change the figcaption output to use Blade's
escaped output (e.g. use {{ $caption }} or {!! e($caption) !!}) so $caption is
escaped before rendering, keeping the existing trim($caption) check and the
surrounding `@if` block intact.

Comment thread packages/visual-editor-renderer-blade/src/BlockRenderer.php Outdated
@ViewFromTheBox ViewFromTheBox moved this from Backlog to In Review in ArtisanPack UI Overview Apr 20, 2026
@ViewFromTheBox
ViewFromTheBox marked this pull request as ready for review April 20, 2026 14:01
@ViewFromTheBox ViewFromTheBox linked an issue Apr 20, 2026 that may be closed by this pull request
8 tasks
Apply the subset of CodeRabbit's findings that are genuine improvements
without fighting the block model (RichText-shaped attributes like
`content`, `caption`, `summary`, `citation`, `value`, `text` are stored
as HTML by editor design and still render raw — same contract as
Gutenberg's save.js; trust boundary is M3's `Gate::authorize('update')`).

Security hardening
- New `UrlSanitizer::safe()` helper + `SAFE_SCHEMES` allowlist (http,
  https, mailto, tel, ftp, sms). Relative URLs pass through.
- button/image/file partials route every href/src through UrlSanitizer,
  so `javascript:`, `data:`, `vbscript:` etc. drop out before they can
  reach the DOM.
- button: auto-append `noopener noreferrer` to `rel` whenever
  `target="_blank"` to prevent tabnabbing.
- cover: clamp `dimRatio` to 0–100, whitelist `minHeightUnit` to
  (px, em, rem, vh, vw, %), cast `minHeight` to a non-negative float.
- table: validate per-cell `align` against (left, center, right,
  justify) before interpolating into inline CSS.
- BlockRenderer: wrap dynamic-block validate/render in try/catch so a
  misbehaving DynamicBlock falls back to the static partial instead of
  taking down the surrounding page.

Correctness fixes
- media-text: `grid-template-columns` for `mediaPosition=right` now uses
  `$mediaWidth` (not `100 - $mediaWidth`) — previously inverted.
- search: treat `buttonUseIcon` as a boolean flag (was casting to "1"
  when true) and generate per-instance unique input/label ids so
  multiple search blocks on one page don't collide.
- column: preserve fractional widths (e.g. 33.33%) instead of truncating
  to int.
- buttons: don't seed `is-content-justification-left` up front; pick
  exactly one justification class based on the configured value.

Tests
- 10 new tests in tests/Unit/BlockRendererTest.php covering each fix
  above: URL scheme dropping, target=_blank rel enforcement, cover
  clamping, table align allowlist, unique search ids, boolean
  buttonUseIcon, fractional column widths, dynamic-block exception
  recovery, and the media-text right-column regression.

Findings explicitly not applied
- Raw-HTML "XSS" callouts for RichText fields (paragraph/heading/quote/
  list/caption/summary) — intentional rendering of editor-produced
  HTML. Escaping would break bold/italic/link formatting. Writes are
  gated by M3 authorization.
- `wp_kses_post()` suggestions — WordPress-only, not available in Laravel.
- normalizeHtml() test helper — collapses whitespace in text nodes, but
  it's a test-only utility and all tests rely on current behavior.
- PACKAGING.md path example — an illustrative local path, accurate for
  the monorepo layout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ViewFromTheBox
ViewFromTheBox merged commit 1fd4985 into release/1.0 Apr 20, 2026
1 check passed
@ViewFromTheBox
ViewFromTheBox deleted the feature/319-m9-blade-renderer-package branch April 20, 2026 14:21
@github-project-automation github-project-automation Bot moved this from In Review to Done in ArtisanPack UI Overview Apr 20, 2026
@coderabbitai coderabbitai Bot mentioned this pull request Jun 5, 2026
5 of 13 tasks
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.

M9 · Blade renderer package

1 participant