Repository navigation
feat: M9 · Blade renderer package - #336
Conversation
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>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (12)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (8)
📝 WalkthroughWalkthroughA new Laravel package, Changes
Sequence DiagramsequenceDiagram
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 !!}
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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 | 🟡 MinorReplace the machine-specific path example with a portable placeholder.
Line 62hardcodes 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 | 🟡 MinorAvoid 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 | 🟡 MinorPreserve fractional widths when computing
flex-basis.Line 16 truncates numeric widths to integers, so values like
33.33render as33%.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 8currently 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 1intentionally renders unescaped HTML. Add an inline note (or matching README note) that$htmlmust 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: MakeisStackedOnMobilehandling explicit.Line 4 mixes
empty()andisset(), 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 --forcecan 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
📒 Files selected for processing (42)
PACKAGING.mdcomposer.jsonpackages/visual-editor-renderer-blade/README.mdpackages/visual-editor-renderer-blade/composer.jsonpackages/visual-editor-renderer-blade/resources/views/blocks/core/audio.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/button.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/buttons.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/code.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/column.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/columns.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/cover.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/details.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/embed.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/file.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/gallery.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/group.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/heading.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/image.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/list-item.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/list.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/media-text.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/paragraph.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/preformatted.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/pullquote.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/quote.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/row.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/search.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/separator.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/spacer.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/stack.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/table.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/verse.blade.phppackages/visual-editor-renderer-blade/resources/views/blocks/core/video.blade.phppackages/visual-editor-renderer-blade/resources/views/components/blocks.blade.phppackages/visual-editor-renderer-blade/src/BlockRenderer.phppackages/visual-editor-renderer-blade/src/View/Components/BlocksComponent.phppackages/visual-editor-renderer-blade/src/VisualEditorRendererBladeServiceProvider.phppackages/visual-editor-renderer-blade/tests/Feature/BlocksComponentTest.phppackages/visual-editor-renderer-blade/tests/TestCase.phppackages/visual-editor-renderer-blade/tests/Unit/BlockRendererTest.phpphpunit.xmltests/Pest.php
| @if ( '' !== trim( $caption ) ) | ||
| <figcaption>{!! $caption !!}</figcaption> |
There was a problem hiding this comment.
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.
| $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 !!} |
There was a problem hiding this comment.
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.
| $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.
| $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 ); | ||
| }; |
There was a problem hiding this comment.
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).
| $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 ) ) | ||
| : ''; |
There was a problem hiding this comment.
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.
| $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> |
There was a problem hiding this comment.
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.
| $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.
| @if ( '' !== trim( $caption ) ) | ||
| <figcaption>{!! $caption !!}</figcaption> | ||
| @endif |
There was a problem hiding this comment.
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.
| @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.
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>
Description
Ships the new
artisanpack-ui/visual-editor-renderer-bladesub-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 registeredDynamicBlock::render()inline with no extra HTTP round trip.The sub-package lives inside this monorepo under
packages/visual-editor-renderer-blade/with its own standalonecomposer.json. Packagist distribution is documented in the newPACKAGING.mdat the repo root — the subtree-split wiring itself is deferred to M15 (Docs, website, release), same approach Laravel Framework uses forilluminate/*.Closes: #319
Type of Change
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:
…and every block in the frozen V1 allow-list renders as the expected semantic HTML, while dynamic blocks (e.g.
LatestPostsBlock) transparently call their PHPrender()method.Changes Made
artisanpack-ui/visual-editor-renderer-bladeatpackages/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 thevisual-editor-blade-viewstag.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-postsis dynamic-only so no static partial ships.README.mdwith usage + override instructions.composer.jsonautoloads the sub-package'ssrc/+tests/.phpunit.xmlgains aRendererBladetestsuite.tests/Pest.phpbinds the sub-package'sTestCaseto its own test paths.PACKAGING.mdat the repo root documenting the splitsh-based Packagist distribution plan for M15.~/Herd/artisanpack-ui): newGET /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:
release/1.0integration branchTests Performed:
/packages/visual-editor/m9/post/{id}route and verified the saved tree rendered as expected semantic HTML.<div>without breaking layout../vendor/bin/pest) — 126/126 passing, including the 28 new renderer-blade tests.Accessibility Tests Run
<figure>,<blockquote>,<details>, etc.) rather than divs where possible.aria-hiddenset on the cover block's overlay + spacer;role="search"on the search form.Tests Added
tests/Unit/BlockRendererTest.php— 24 tests covering every partial category, dynamic block delegation, validation hooks, view coercion, fallback escaping).tests/Feature/BlocksComponentTest.php— 4 tests covering the component from array/JSON trees, null tree, and thevisual-editor-blade-viewspublish tag).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
README.md.Documentation details:
PACKAGING.mdat 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
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).🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Tests
Chores