Skip to content

feat: A4 · Custom block scaffolding + reference block - #352

Merged
ViewFromTheBox merged 2 commits into
release/1.0from
feature/346-custom-block-scaffolding-reference-block
Apr 21, 2026
Merged

ViewFromTheBox merged 2 commits into
release/1.0from
feature/346-custom-block-scaffolding-reference-block

Conversation

@ViewFromTheBox

@ViewFromTheBox ViewFromTheBox commented Apr 21, 2026 •

Copy link
Copy Markdown
Member

Description

Ships the documented pattern + tooling for host apps to register custom blocks under the artisanpack/* namespace, plus one reference block (artisanpack/callout) that exercises the pattern end to end. V2 block forking (#331) is not in scope here.

Closes: #346

Type of Change

  • New feature (adds new functionality)

Related Issue

Issue: #346

Motivation and Context

Pre-PR, zero ArtisanPack custom blocks existed. DynamicBlock / ClosureDynamicBlock supported server-rendered blocks, but there was no edit-side authoring pattern or reference static block and no artisanpack category in the inserter. A4 closes that gap.

Changes Made

PHP

  • Widen VisualEditor::registerBlock() to accept three input shapes: a block.json path, a class implementing the new ProvidesBlockMetadata interface, or a closure returning the metadata array.
  • Register the bundled callout via registerReferenceBlocks() in VisualEditorServiceProvider; add artisanpack/callout to the default enabled_blocks allow-list.

JS

  • New editor/custom-blocks.ts: eager import.meta.glob auto-discovery over resources/js/visual-editor/blocks/*/index.ts, artisanpack block-category registration, and an optional inline-SVG icon override (the editor does not bundle dashicons.css, so dashicon-slug icons render blank).
  • Wired discoverAndRegisterCustomBlocks() into registerOnce() in editor-app.tsx.

Reference block

  • blocks/callout/: block.json with severity + icon enum attributes, edit.tsx (InspectorControls + RichText), matching save.tsx, scoped CSS, and an inline-SVG inserter icon.

Renderers (Blade / React / Vue)

  • Each renderer package now ships an artisanpack/callout partial/component that mirrors the save.tsx markup so server-rendered and client-rendered pages emit identical HTML.

Docs

  • docs/custom-blocks.md: authoring pattern, directory structure, block.json schema, dynamic vs static guidance, per-renderer partial pattern, and a per-block checklist. Feeds into M15 · Docs, website, release #325 M15.

How Has This Been Tested?

Testing Environment:

  • Operating System: macOS (Darwin 25.3.0)
  • Browser (if applicable): tested in the dev app via Laravel Herd
  • PHP Version: 8.4.3
  • Project Version: 1.0.0

Tests Performed:

  1. Automated: full Pest suite + full Vitest suite.
  2. Manual: opened the editor in the dev app, confirmed the ArtisanPack category shows up in the block library, inserted a Callout, swapped severity + icon through InspectorControls, verified the canvas re-rendered with the matching class / SVG, and confirmed edit/save round-trips on reload.

Accessibility Tests Run

  • Keyboard navigation tested — InspectorControls SelectControl components use the standard Gutenberg focus pattern, inserter entries remain keyboard-navigable.
  • Screen reader tested
  • Color contrast verified — severity variants use the same token palette already in the editor theme.
  • ARIA labels checked — inserter icon, block icon, and RichText placeholder are all tagged aria-hidden="true" on decorative elements; the RichText body carries the implicit role.

Tests Added

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

Test details:

  • PHP: tests/Unit/VisualEditor/RegisterBlockTest.php covers all three registration forms + error paths; BlockRendererTest gains Blade renderer cases for the callout.
  • JS: resources/js/visual-editor/editor/__tests__/custom-blocks.test.tsx covers category registration, the registerCustomBlocks pipeline (dedupe + icon override + skip-missing-name), and the callout edit/save round-trip; React and Vue renderer BlockTree.test files gain callout cases.
  • Totals: 176 PHP tests / 515 JS tests passing.

Documentation

  • Inline code documentation added
  • API documentation updated

Documentation details:
docs/custom-blocks.md is the new authoring guide — registration mechanics, schema notes, dynamic vs static, renderer wiring, and an authoring checklist.

Screenshots

(available on request; manually verified in the dev app — ArtisanPack category + Callout inserter icon + severity/icon InspectorControls all render as expected.)

Pre-Submission Checklist

  • Followed contributing guidelines
  • Checked for other open PRs for same update
  • Code passes all tests
  • Code has been linted (no lint config in this package; npx tsc --noEmit surfaces only pre-existing @wordpress/* missing-types warnings)
  • Accessibility tests completed
  • Code follows project style guide
  • Self-review completed
  • Comments added for complex code (glob transform + Dashicon CSS notes inline in custom-blocks.ts)
  • No new warnings generated

Summary by CodeRabbit

  • New Features

    • Added a "Callout" block to the visual editor (Info/Success/Warning/Error) with selectable icons, editor controls, saved markup, and inserter availability.
  • Documentation

    • Added a comprehensive guide for authoring custom visual editor blocks (layout, metadata, registration, rendering, checklist).
  • Tests

    • Added unit and integration tests covering callout rendering, normalization/fallbacks, and custom-block registration/discovery.

Ship the documented pattern + tooling for host apps to register custom
blocks under the `artisanpack/*` namespace, plus one reference block
(`artisanpack/callout`) that exercises the pattern end to end.

PHP
- Widen `VisualEditor::registerBlock()` to accept a block.json path,
  a class implementing the new `ProvidesBlockMetadata` interface, or
  a closure returning the metadata array
- Register the bundled callout via `registerReferenceBlocks()` in the
  service provider; add it to the default `enabled_blocks` allow-list

JS
- Add `editor/custom-blocks.ts`: auto-discovery via `import.meta.glob`
  over `resources/js/visual-editor/blocks/*/index.ts`, `artisanpack`
  block-category registration, and a `CustomBlockModule` contract that
  forwards an optional inline-SVG `icon` override to `registerBlockType`
  (the editor does not bundle `dashicons.css`, so a dashicon-slug icon
  would render blank)
- Wire the discovery call into `registerOnce()` in `editor-app.tsx`

Reference block
- `blocks/callout/`: `block.json` with severity + icon enum attributes,
  `edit.tsx` with `InspectorControls` + `RichText`, matching `save.tsx`,
  scoped CSS, and an inline-SVG inserter icon

Renderers
- Blade, React, and Vue renderer packages each gain a
  `blocks/artisanpack/callout` partial mirroring the `save.tsx` markup
  so server-rendered and client-rendered pages ship identical HTML

Tests + docs
- PHP: `RegisterBlockTest` covering all three registration forms +
  error paths; Blade renderer tests for the callout
- JS: `custom-blocks.test.tsx` covering the registration pipeline +
  callout round-trip; React and Vue renderer BlockTree tests for the
  callout
- `docs/custom-blocks.md`: authoring pattern, schema notes, dynamic
  vs static guidance, per-renderer partial pattern, checklist

Closes #346

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

coderabbitai Bot commented Apr 21, 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: f78e390b-38ae-40ee-925b-fd2e114072d7

📥 Commits

Reviewing files that changed from the base of the PR and between f508e9d and 90ed48e.

📒 Files selected for processing (5)
  • docs/custom-blocks.md
  • resources/js/visual-editor/blocks/callout/block.json
  • resources/js/visual-editor/blocks/callout/callout.css
  • resources/js/visual-editor/editor/custom-blocks.ts
  • src/VisualEditor.php
✅ Files skipped from review due to trivial changes (3)
  • resources/js/visual-editor/blocks/callout/block.json
  • resources/js/visual-editor/blocks/callout/callout.css
  • docs/custom-blocks.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • resources/js/visual-editor/editor/custom-blocks.ts
  • src/VisualEditor.php

📝 Walkthrough

Walkthrough

Adds a custom-block scaffolding + discovery system and ships a reference block artisanpack/callout with editor metadata, JS editor/save, Blade/React/Vue renderers, registration APIs (path/class/closure), docs, and tests exercising end-to-end registration and rendering.

Changes

Cohort / File(s) Summary
Config & Docs
config/visual-editor.php, docs/custom-blocks.md
Enabled artisanpack/callout in allow-list; added comprehensive docs describing directory layout, block.json schema, JS auto-discovery, PHP registration forms (path/class/closure), and renderer guidance.
JS: Callout Block (editor & build)
resources/js/visual-editor/blocks/callout/* (includes block.json, index.ts, edit.tsx, save.tsx, icons.tsx, inserter-icon.tsx, callout.css)
New Gutenberg block metadata, editor UI (Inspector + RichText), save serializer, icon registry, inserter icon, and styles for artisanpack/callout.
JS: Auto-discovery & registration
resources/js/visual-editor/editor/custom-blocks.ts, resources/js/visual-editor/editor/editor-app.tsx, resources/js/visual-editor/editor/__tests__/custom-blocks.test.tsx
Added Vite import.meta.glob based discovery, category ensurement, module validation/registration, integration into editor startup, and tests covering registration and inspector behavior.
Renderers: Blade / React / Vue
packages/visual-editor-renderer-blade/.../blocks/artisanpack/callout.blade.php, packages/visual-editor-renderer-react/src/blocks/artisanpack/callout.tsx, packages/visual-editor-renderer-vue/src/blocks/artisanpack/callout.ts, plus respective registerCoreBlocks updates and renderer tests
Implemented callout renderers in Blade/React/Vue with severity/icon normalization, SVG icon lookup, class composition, and tests verifying valid and fallback rendering.
React/Vue core registry
packages/visual-editor-renderer-react/src/blocks/registerCoreBlocks.ts, packages/visual-editor-renderer-vue/src/blocks/registerCoreBlocks.ts
Registered artisanpack/callout renderer entries in core renderer registries.
PHP: Block registration surface & service provider
src/Blocks/ProvidesBlockMetadata.php, src/VisualEditor.php, src/VisualEditorServiceProvider.php, src/Facades/VisualEditor.php
Added ProvidesBlockMetadata interface; refactored VisualEditor::registerBlock() to accept path
PHP Tests & Fixtures
tests/Unit/VisualEditor/RegisterBlockTest.php, tests/Unit/VisualEditor/EnabledBlockNamesTest.php, tests/Fixtures/TestBlockMetadata.php
Added tests covering registration via path/class/closure, error cases, enabled-blocks snapshot update, and a fixture implementing ProvidesBlockMetadata.
JS Tests: block tree / renderer
packages/visual-editor-renderer-react/tests/BlockTree.test.tsx, packages/visual-editor-renderer-vue/tests/BlockTree.test.ts, packages/visual-editor-renderer-blade/tests/Unit/BlockRendererTest.php
Added rendering tests for callout across renderers, asserting class composition, data attributes, icon presence, and fallback normalization.

Sequence Diagram(s)

sequenceDiagram
    participant Host as Host App\n(editor-app.tsx)
    participant Discovery as Auto-Discovery\n(custom-blocks.ts)
    participant Resolver as Module\nResolver
    participant Registry as Block\nRegistry
    participant Editor as Editor\nUI

    Host->>Host: registerOnce()
    Host->>Discovery: discoverAndRegisterCustomBlocks()
    Discovery->>Discovery: import.meta.glob('../blocks/*/index.ts')
    Discovery->>Resolver: resolveCustomBlockModule(module)
    Resolver->>Resolver: Validate metadata.name\nand edit/save/icon exports
    Resolver-->>Discovery: validated module
    Discovery->>Discovery: ensureArtisanpackCategory()
    Discovery->>Registry: registerBlockType() for each module
    Registry->>Registry: Wire edit, save, metadata, icon
    Discovery-->>Host: registered block names
    Host->>Editor: Initialize with registered blocks
    Editor->>Editor: Render inserter menu / blocks
Loading
sequenceDiagram
    participant EditorUI as Block Editor\n(edit.tsx)
    participant State as Block\nAttributes
    participant Renderer as Backend\nRenderer
    participant Output as Rendered\nHTML

    EditorUI->>EditorUI: User changes severity via Inspector
    EditorUI->>State: setAttributes({ severity })
    State-->>EditorUI: attributes updated
    EditorUI->>Renderer: Request render preview (severity, icon, content)
    Renderer->>Renderer: Normalize severity/icon\nCompose classes and select SVG path
    Renderer-->>Output: HTML with icon, classes, content
    Output-->>EditorUI: Display preview in editor
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hopped a trail of metadata bright,
Built callouts in JS, Blade, Vue, and light,
Icons and severities twinkling in view,
A tiny block garden—welcome, artisan crew! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: A4 · Custom block scaffolding + reference block' clearly and concisely summarizes the primary changes: enabling custom block registration with a reference implementation.
Linked Issues check ✅ Passed The PR implements all acceptance criteria from #346: block registration pipeline (PHP/JS), reference callout block with block.json/edit/save, renderer implementations, category registration, documentation, and comprehensive tests covering all three registration methods.
Out of Scope Changes check ✅ Passed All changes are directly aligned with #346 objectives. No unrelated modifications to existing blocks, config changes beyond callout allowlist, or extraneous features were introduced.

✏️ 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/346-custom-block-scaffolding-reference-block

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

@ViewFromTheBox ViewFromTheBox moved this from Backlog to In Review in ArtisanPack UI Overview Apr 21, 2026
@ViewFromTheBox ViewFromTheBox linked an issue Apr 21, 2026 that may be closed by this pull request
17 tasks
@ViewFromTheBox
ViewFromTheBox marked this pull request as ready for review April 21, 2026 19:53

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

🧹 Nitpick comments (1)
resources/js/visual-editor/blocks/callout/edit.tsx (1)

44-50: Derive the icon options from the shared icon registry.

ICON_OPTIONS duplicates the canonical icon names from icons.tsx, so the inspector can drift from the actual renderer/save support later. Pull the values from CALLOUT_ICON_NAMES (or a shared options export) and keep only the label mapping here.

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

In `@resources/js/visual-editor/blocks/callout/edit.tsx` around lines 44 - 50,
ICON_OPTIONS currently hardcodes the callout icon names and duplicates the
canonical list in icons.tsx; change ICON_OPTIONS to derive its values from the
shared CALLOUT_ICON_NAMES (or the exported options array) so the inspector stays
in sync with the renderer/save support. Locate the ICON_OPTIONS constant in
edit.tsx and replace the hardcoded value array with a map over
CALLOUT_ICON_NAMES (importing it from the module that exports it) that returns
objects with label (humanized or title-cased) and value set to the canonical
name; keep the label mapping only in this file and ensure the import of
CALLOUT_ICON_NAMES (or the shared options export) is added and used instead of
duplicating names.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/custom-blocks.md`:
- Around line 23-31: The markdown uses unlabeled fenced code blocks that trigger
markdownlint MD040; update each of the affected triple-backtick fences in
docs/custom-blocks.md (the blocks showing file trees and file paths such as the
resources/js/visual-editor/blocks/ snippet and the packages/.../callout.*
snippets) to include a fence language (use "text") so the fences read ```text;
ensure you change the fences at the occurrences noted (around the snippets for
resources/js/visual-editor/blocks/ and the three packages/.../callout files)
while preserving the inner content exactly.

In
`@packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/callout.blade.php`:
- Around line 17-18: $content is being cast to string from
$attributes['content'] and then emitted with {!! !!}, allowing HTML/script
injection; replace the direct cast with a call to the shared sanitizer (e.g.,
sanitizeHtml or sanitize_block_content) before assigning $content (use the
centralized allow-list sanitizer used by the React/Vue renderers), so $content =
sanitize_block_content($attributes['content'] ?? ''); and ensure the same
sanitizer function is available/imported for use in this Blade render and the
mirrored client renderers.

In `@packages/visual-editor-renderer-react/src/blocks/artisanpack/callout.tsx`:
- Around line 78-81: The JSX for the callout body uses dangerouslySetInnerHTML
with the raw content variable (div.ap-callout__body, dangerouslySetInnerHTML={{
__html: content }}), which is an XSS risk; fix by sanitizing content before
injection (e.g., import and use a sanitizer like DOMPurify and call
DOMPurify.sanitize(content) or run the same sanitizer via a shared helper before
passing into attrString/renderer), and apply the same sanitizer at the renderer
layer for other components that use dangerouslySetInnerHTML (paragraph, heading,
quote, code, preformatted, list-item, figcaption, blockquote, table cells) or
enforce/validate a trusted-HTML contract upstream.

In `@resources/js/visual-editor/blocks/callout/block.json`:
- Around line 28-35: The block.json "supports" enables "anchor" and "spacing"
which causes useBlockProps.save() to inject id and spacing attributes in the
editor but the saved output (see save.tsx and the Blade/React/Vue renderers)
only emits class and data-severity, causing editor attributes to be lost; fix
this by either removing/disabling "anchor" and the "spacing" object from the
"supports" block (so supports no longer includes "anchor" or "spacing") OR
update the frontend renderers and save.tsx to use useBlockProps.save() (or
replicate its behavior) so the rendered output includes the same id and spacing
classes/inline styles as the editor, ensuring attributes like anchor and spacing
are preserved.

In `@resources/js/visual-editor/blocks/callout/callout.css`:
- Around line 49-52: The stylelint error is due to the non-canonical casing of
the color keyword in the .ap-callout__icon svg rule; update the fill value in
the .ap-callout__icon svg CSS rule to use the canonical lowercase keyword
"currentcolor" (replace currentColor with currentcolor) so it satisfies the
linting rule.

In `@resources/js/visual-editor/editor/custom-blocks.ts`:
- Around line 170-176: The current selection logic for candidate prefers any
object-shaped module.default over named exports (variable candidate built from
module and type GlobbedModule), which can skip valid named-export modules;
change the logic in the candidate resolution so you only choose module.default
when it actually matches the expected block shape (validate required block
properties/signature on module.default, e.g., expected keys/functions that
define a block) and otherwise fall back to using module (the named exports).
Update the check around module.default and the usage of GlobbedModule to perform
a defensive shape/feature test rather than a plain typeof object test so valid
named exports are not ignored.

In `@src/VisualEditor.php`:
- Around line 77-83: Trim and normalize the block name before registering:
replace the current check on $metadata['name'] with code that trims the value
once, splits it on '/' and ensures there are exactly two non-empty segments
(namespace and name), throw the same InvalidArgumentException if not valid, then
register the normalized joined string (e.g. $normalizedName = $segments[0] . '/'
. $segments[1]) via $this->registry->register($normalizedName, $metadata)
instead of using the raw $metadata['name'].

---

Nitpick comments:
In `@resources/js/visual-editor/blocks/callout/edit.tsx`:
- Around line 44-50: ICON_OPTIONS currently hardcodes the callout icon names and
duplicates the canonical list in icons.tsx; change ICON_OPTIONS to derive its
values from the shared CALLOUT_ICON_NAMES (or the exported options array) so the
inspector stays in sync with the renderer/save support. Locate the ICON_OPTIONS
constant in edit.tsx and replace the hardcoded value array with a map over
CALLOUT_ICON_NAMES (importing it from the module that exports it) that returns
objects with label (humanized or title-cased) and value set to the canonical
name; keep the label mapping only in this file and ensure the import of
CALLOUT_ICON_NAMES (or the shared options export) is added and used instead of
duplicating names.
🪄 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: 77340152-241c-4d6e-a9dd-3a8e667df701

📥 Commits

Reviewing files that changed from the base of the PR and between b9e01db and f508e9d.

📒 Files selected for processing (27)
  • config/visual-editor.php
  • docs/custom-blocks.md
  • packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/callout.blade.php
  • packages/visual-editor-renderer-blade/tests/Unit/BlockRendererTest.php
  • packages/visual-editor-renderer-react/src/blocks/artisanpack/callout.tsx
  • packages/visual-editor-renderer-react/src/blocks/registerCoreBlocks.ts
  • packages/visual-editor-renderer-react/tests/BlockTree.test.tsx
  • packages/visual-editor-renderer-vue/src/blocks/artisanpack/callout.ts
  • packages/visual-editor-renderer-vue/src/blocks/registerCoreBlocks.ts
  • packages/visual-editor-renderer-vue/tests/BlockTree.test.ts
  • resources/js/visual-editor/blocks/callout/block.json
  • resources/js/visual-editor/blocks/callout/callout.css
  • resources/js/visual-editor/blocks/callout/edit.tsx
  • resources/js/visual-editor/blocks/callout/icons.tsx
  • resources/js/visual-editor/blocks/callout/index.ts
  • resources/js/visual-editor/blocks/callout/inserter-icon.tsx
  • resources/js/visual-editor/blocks/callout/save.tsx
  • resources/js/visual-editor/editor/__tests__/custom-blocks.test.tsx
  • resources/js/visual-editor/editor/custom-blocks.ts
  • resources/js/visual-editor/editor/editor-app.tsx
  • src/Blocks/ProvidesBlockMetadata.php
  • src/Facades/VisualEditor.php
  • src/VisualEditor.php
  • src/VisualEditorServiceProvider.php
  • tests/Fixtures/TestBlockMetadata.php
  • tests/Unit/VisualEditor/EnabledBlockNamesTest.php
  • tests/Unit/VisualEditor/RegisterBlockTest.php

Comment thread docs/custom-blocks.md Outdated
Comment on lines +17 to +18
$content = (string) ( $attributes['content'] ?? '' );

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

Sanitize content before rendering it as HTML.

$attributes['content'] is only coerced to string here, then emitted with {!! !!} on Line 42. That makes the callout body an HTML injection sink; a crafted block tree can ship script/event-handler markup straight through the Blade renderer. The mirrored React/Vue renderers have the same trust boundary, so this needs a shared sanitizer/allow-list rather than a one-off escape.

Also applies to: 42-42

🤖 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/artisanpack/callout.blade.php`
around lines 17 - 18, $content is being cast to string from
$attributes['content'] and then emitted with {!! !!}, allowing HTML/script
injection; replace the direct cast with a call to the shared sanitizer (e.g.,
sanitizeHtml or sanitize_block_content) before assigning $content (use the
centralized allow-list sanitizer used by the React/Vue renderers), so $content =
sanitize_block_content($attributes['content'] ?? ''); and ensure the same
sanitizer function is available/imported for use in this Blade render and the
mirrored client renderers.

Comment on lines +78 to +81
<div
className="ap-callout__body"
dangerouslySetInnerHTML={{ __html: content }}
/>

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect all raw HTML injection sites in the React renderer package.
rg -n -C3 --type=tsx 'dangerouslySetInnerHTML' packages/visual-editor-renderer-react/src

# 2) Check whether sanitization utilities exist and are used on the renderer path.
rg -n -C3 --type=ts --type=tsx 'sanitize|DOMPurify|xss|escapeHtml|allowedTags|kses' \
  packages/visual-editor-renderer-react/src resources/js/visual-editor

# 3) Trace this block's content flow.
rg -n -C3 --type=tsx --type=ts 'CalloutBlock|attributes\.content|block\/callout' \
  packages/visual-editor-renderer-react/src resources/js/visual-editor

Repository: ArtisanPack-UI/visual-editor

Length of output: 99


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Read the callout.tsx file to see full context
echo "=== Content of callout.tsx ==="
cat -n packages/visual-editor-renderer-react/src/blocks/artisanpack/callout.tsx

echo ""
echo "=== Search for dangerouslySetInnerHTML (no type filter) ==="
rg -n -C3 'dangerouslySetInnerHTML' packages/visual-editor-renderer-react/src --glob='*.tsx' --glob='*.ts'

echo ""
echo "=== Search for sanitization utilities ==="
rg -n 'sanitize|DOMPurify|xss|escapeHtml|allowedTags|kses' packages/visual-editor-renderer-react/src resources/js/visual-editor --glob='*.tsx' --glob='*.ts' --glob='*.js' || true

echo ""
echo "=== Check package.json for security libraries ==="
cat packages/visual-editor-renderer-react/package.json | grep -A 5 -B 5 'depend' || true

Repository: ArtisanPack-UI/visual-editor

Length of output: 20207


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check attrString implementation ==="
find packages/visual-editor-renderer-react/src -name 'attributes*' -o -name 'support*' | head -20

echo ""
echo "=== Read the attributes support file ==="
cat -n packages/visual-editor-renderer-react/src/support/attributes.ts 2>/dev/null || echo "File not found"

echo ""
echo "=== Check package.json dependencies ==="
cat packages/visual-editor-renderer-react/package.json

Repository: ArtisanPack-UI/visual-editor

Length of output: 4465


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check BlockRendererProps type definition ==="
rg -n 'BlockRendererProps' packages/visual-editor-renderer-react/src --glob='*.ts' --glob='*.tsx' -A 5

echo ""
echo "=== Check for block JSON sources in visual-editor ==="
find resources/js/visual-editor -name '*.ts' -o -name '*.tsx' | head -10

echo ""
echo "=== Search for where callout block attributes are set ==="
rg -n 'callout.*content\|attributes\.content' resources/js/visual-editor --glob='*.ts' --glob='*.tsx' --glob='*.js' -B 3 -A 3 || true

echo ""
echo "=== Check if there's HTML sanitization in the editor ==="
rg -n 'DOMPurify\|sanitizeHtml\|sanitize.*content\|allowedTags' resources/ --glob='*.ts' --glob='*.tsx' --glob='*.js' || true

Repository: ArtisanPack-UI/visual-editor

Length of output: 26262


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check Blade callout renderer ==="
find . -name 'callout.blade.php' 2>/dev/null | head -5

echo ""
echo "=== Search for where block JSON is created/parsed in editor ==="
rg -n 'attributes.*save\|saveBlock\|block.*JSON' resources/js/visual-editor --glob='*.ts' --glob='*.tsx' -B 2 -A 2 | head -50

echo ""
echo "=== Check if there's an API layer that creates block data ==="
find . -name '*.php' -type f | xargs grep -l 'block.*attributes\|dangerouslySetInnerHTML' 2>/dev/null | head -10

Repository: ArtisanPack-UI/visual-editor

Length of output: 266


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Read Blade callout renderer ==="
cat -n packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/callout.blade.php

echo ""
echo "=== Check if Blade uses any sanitization in block views ==="
rg -n 'e(\|trim\|Purifier\|sanitize' packages/visual-editor-renderer-blade/resources/views --glob='*.blade.php' -A 2 -B 2 | head -40

Repository: ArtisanPack-UI/visual-editor

Length of output: 2104


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check block edit/save in editor to understand data flow ==="
find resources/js/visual-editor -name 'callout' -type d

echo ""
echo "=== List callout block editor files ==="
ls -la resources/js/visual-editor/_legacy/editor/blocks/callout/ 2>/dev/null || ls -la resources/js/visual-editor/blocks/callout/ 2>/dev/null || echo "Not found in those paths"

echo ""
echo "=== Check for sanitization in any block editor ==="
rg -n 'sanitize\|DOMPurify\|allowedTags' resources/js/visual-editor --glob='*.ts' --glob='*.tsx' --glob='*.js' 2>/dev/null | head -20

Repository: ArtisanPack-UI/visual-editor

Length of output: 803


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check callout block save component ==="
cat -n resources/js/visual-editor/blocks/callout/save.tsx

echo ""
echo "=== Check callout block edit component ==="
cat -n resources/js/visual-editor/blocks/callout/edit.tsx

echo ""
echo "=== Check if any editor blocks sanitize content on save ==="
rg -n 'RichText\|contentToSave\|save.*content' resources/js/visual-editor/blocks --glob='*.tsx' -A 3 -B 1 | head -60

Repository: ArtisanPack-UI/visual-editor

Length of output: 6620


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check registry documentation ==="
cat -n packages/visual-editor-renderer-react/src/registry.ts | head -30

echo ""
echo "=== Check README for security notes ==="
cat packages/visual-editor-renderer-react/README.md 2>/dev/null || echo "No README found"

echo ""
echo "=== Check if block JSON is validated anywhere ==="
rg -n 'validate\|schema\|json.?schema' resources/js/visual-editor/blocks/callout --glob='*.json' --glob='*.ts' --glob='*.tsx' -B 1 -A 1

echo ""
echo "=== Check block.json schema ==="
cat -n resources/js/visual-editor/blocks/callout/block.json

Repository: ArtisanPack-UI/visual-editor

Length of output: 5441


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check block.json ==="
cat -n resources/js/visual-editor/blocks/callout/block.json

echo ""
echo "=== Check how BlockTree receives data - look for entry point ==="
cat -n packages/visual-editor-renderer-react/src/BlockTree.tsx | head -80

echo ""
echo "=== Check where block JSON originates from Inertia ==="
rg -n 'post\.content\|BlockTree\|HasBlockContent' --glob='*.tsx' --glob='*.ts' --glob='*.php' -B 2 -A 2 | head -40

Repository: ArtisanPack-UI/visual-editor

Length of output: 4665


Sanitize HTML before using dangerouslySetInnerHTML.

Line 80 injects attributes.content as raw HTML without sanitization. The attrString() utility only type-coerces; it does not sanitize. Since block attributes are typed as Record<string, unknown> at runtime with no validation layer, if block JSON can originate from untrusted sources (API, user input), this is an XSS vector.

This pattern is systemic across the React renderer: paragraph, heading, quote, code, preformatted, list-item, figcaption, blockquote, table cells, and more all use dangerouslySetInnerHTML without sanitization. Consider:

  • Adding DOMPurify or a similar sanitization library and sanitizing content at the renderer level
  • Or, explicitly document and enforce a strict "trusted-HTML-only" contract with validation upstream (e.g., in the editor's save layer or a block JSON validator)
🧰 Tools
🪛 ast-grep (0.42.1)

[warning] 79-79: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

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

In `@packages/visual-editor-renderer-react/src/blocks/artisanpack/callout.tsx`
around lines 78 - 81, The JSX for the callout body uses dangerouslySetInnerHTML
with the raw content variable (div.ap-callout__body, dangerouslySetInnerHTML={{
__html: content }}), which is an XSS risk; fix by sanitizing content before
injection (e.g., import and use a sanitizer like DOMPurify and call
DOMPurify.sanitize(content) or run the same sanitizer via a shared helper before
passing into attrString/renderer), and apply the same sanitizer at the renderer
layer for other components that use dangerouslySetInnerHTML (paragraph, heading,
quote, code, preformatted, list-item, figcaption, blockquote, table cells) or
enforce/validate a trusted-HTML contract upstream.

Comment thread resources/js/visual-editor/blocks/callout/block.json Outdated
Comment thread resources/js/visual-editor/blocks/callout/callout.css Outdated
Comment thread resources/js/visual-editor/editor/custom-blocks.ts Outdated
Comment thread src/VisualEditor.php Outdated
Comment on lines +77 to +83
if ( ! isset( $metadata['name'] ) || ! is_string( $metadata['name'] ) || '' === trim( $metadata['name'] ) ) {
throw new InvalidArgumentException(
'Block metadata is missing a non-empty "name" field.'
);
}

$this->registry->register( $metadata['name'], $metadata );

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

Validate and normalize the block name before registration.

This branch only checks that name is a non-empty string, but the new API contract and docblock both require namespace/name. Right now values like callout, foo/bar/baz, or ' artisanpack/callout ' will still be registered. Please trim once, require exactly two non-empty segments, and register the normalized value.

Suggested fix
-		if ( ! isset( $metadata['name'] ) || ! is_string( $metadata['name'] ) || '' === trim( $metadata['name'] ) ) {
+		$name = is_string( $metadata['name'] ?? null ) ? trim( $metadata['name'] ) : '';
+		$parts = explode( '/', $name );
+
+		if ( '' === $name || 2 !== count( $parts ) || '' === $parts[0] || '' === $parts[1] ) {
 			throw new InvalidArgumentException(
-				'Block metadata is missing a non-empty "name" field.'
+				'Block metadata must contain a valid "namespace/name" value.'
 			);
 		}
 
-		$this->registry->register( $metadata['name'], $metadata );
+		$metadata['name'] = $name;
+		$this->registry->register( $name, $metadata );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/VisualEditor.php` around lines 77 - 83, Trim and normalize the block name
before registering: replace the current check on $metadata['name'] with code
that trims the value once, splits it on '/' and ensures there are exactly two
non-empty segments (namespace and name), throw the same InvalidArgumentException
if not valid, then register the normalized joined string (e.g. $normalizedName =
$segments[0] . '/' . $segments[1]) via
$this->registry->register($normalizedName, $metadata) instead of using the raw
$metadata['name'].

- docs/custom-blocks.md: label unlabeled file-tree/path fenced blocks
  with `text` to satisfy markdownlint MD040
- blocks/callout/block.json: drop `anchor` and `spacing` from
  `supports` — the Blade/React/Vue renderers don't round-trip either
  of them, so leaving them enabled would lose editor-set values on the
  frontend
- blocks/callout/callout.css: canonicalize `currentColor` → `currentcolor`
  (stylelint `color-named` rule)
- editor/custom-blocks.ts: tighten the default-vs-named export
  resolution — only prefer `module.default` when it actually carries
  the block shape (metadata.name + edit function), otherwise fall
  back to the named exports on the module itself
- VisualEditor::registerBlock(): trim the incoming name once and
  write the normalized value back into the metadata array before
  registering, so `BlockTypeRegistry` stores a canonical value in
  both the registry key and the stored definition's own `name` field

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

A4 · Custom block scaffolding + reference block

1 participant