Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,22 @@ The V1 editor adopts the upstream `@wordpress/*` packages. Some of those package

Both shims will be replaced by `artisanpack-ui/cms-framework` (the real Laravel-backed `core-data` store and media bridge). Every selector or filter we implement here is one we have to re-verify against Gutenberg upgrades — expand the surface only when an observed crash demands it.

## Block defaults

V1 ships with a frozen allow-list of blocks from `@wordpress/block-library`. The defaults in `config/visual-editor.php` expose only blocks that render correctly against the empty-state `@wordpress/core-data` shim — content, media, layout, and simple widget blocks that do not need a WordPress backend to work.

**Disabled by default.** Every block in the following categories is listed in `disabled_blocks` because it relies on data the shim does not provide:

- **Site / theme** — `core/navigation`, `core/site-logo`, `core/site-title`, `core/site-tagline`, `core/template-part`
- **Query loop** — `core/query`, `core/query-loop`
- **Post context** — `core/post-content`, `core/post-title`, `core/post-excerpt`, `core/post-date`, `core/post-author`, `core/post-featured-image`
- **Taxonomy widgets** — `core/categories`, `core/tag-cloud`, `core/archives`
- **Comments feeds** — `core/latest-comments`

These blocks will be re-enabled once `artisanpack-ui/cms-framework` replaces the shim with a real Laravel-backed `core-data` store. The full per-block classification — including blocks that render empty but do not crash — lives in [`docs/block-library-audit.md`](docs/block-library-audit.md). Because the defaults populate `enabled_blocks` as an explicit allow-list, new blocks introduced by a future `@wordpress/block-library` upgrade are implicitly disabled until the audit is revisited.

Override the defaults by publishing the config to `config/artisanpack/visual-editor.php` and editing the `enabled_blocks` / `disabled_blocks` arrays. The deny-list always wins over the allow-list.

## i18n

Editor strings use `@wordpress/i18n` with the `artisanpack-visual-editor` text domain. The domain is initialized via `bootI18n()` in `resources/js/visual-editor/vendor/i18n.ts`.
Expand Down
57 changes: 55 additions & 2 deletions config/visual-editor.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,64 @@
| always-applied deny-list. The deny-list wins when both are set. Use
| fully-qualified block names (e.g. `core/paragraph`, `core/query`).
|
| The frozen V1 defaults follow the M5 block-library audit
| (see docs/block-library-audit.md). Only blocks that render correctly
| against the empty-state @wordpress/core-data shim are enabled; every
| block that needs a real Laravel-backed core store — navigation, query,
| post-*, site-*, template-part, taxonomy widgets — is disabled until
| the artisanpack-ui/cms-framework package replaces the shim.
|
*/

'enabled_blocks' => [],
'enabled_blocks' => [
'core/paragraph',
'core/heading',
'core/list',
'core/quote',
'core/code',
'core/preformatted',
'core/pullquote',
'core/verse',
'core/table',
'core/image',
'core/gallery',
'core/video',
'core/audio',
'core/file',
'core/embed',
'core/cover',
'core/media-text',
'core/columns',
'core/group',
'core/row',
'core/stack',
'core/buttons',
'core/separator',
'core/spacer',
'core/details',
'core/search',
'core/latest-posts',
],

'disabled_blocks' => [],
'disabled_blocks' => [
'core/navigation',
'core/query',
'core/query-loop',
'core/post-content',
'core/post-title',
'core/post-excerpt',
'core/post-date',
'core/post-author',
'core/post-featured-image',
'core/site-logo',
'core/site-title',
'core/site-tagline',
'core/template-part',
'core/latest-comments',
'core/archives',
'core/categories',
'core/tag-cloud',
],

/*
|--------------------------------------------------------------------------
Expand Down
210 changes: 210 additions & 0 deletions docs/block-library-audit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
# Block Library Audit

Classification of every block registered by `@wordpress/block-library` (v9.44.0)
against the current editor's empty-state `@wordpress/core-data` shim
(`resources/js/visual-editor/vendor/core-data-shim.ts`).

Each block falls into one of three buckets:

- **green** — inserts, renders, and edits without a backing WordPress data
store. Safe to expose in the default `enabled_blocks` list.
- **empty-state** — inserts and renders without crashing, but every data
selector returns `null` or `[]`, so the block shows an empty shell. Shipped
as **disabled-by-default** until the `artisanpack-ui/cms-framework` package
provides a real Laravel-backed `core` store.
- **crash** — throws on insert or first render because the shim does not
implement the selector or action the block expects. Permanently in
`disabled_blocks` until the offending surface is implemented.

The frozen config defaults follow this audit exactly — see
`config/visual-editor.php`.

## Green — enabled by default

These blocks render correctly against the shim and are exposed to the
inserter out of the box.

### Content

| Block | Notes |
| --- | --- |
| `core/paragraph` | Baseline text block. |
| `core/heading` | Renders h1–h6. |
| `core/list` | Container for list items. |
| `core/quote` | Blockquote wrapper. |
| `core/code` | Preformatted code. |
| `core/preformatted` | Plain preformatted text. |
| `core/pullquote` | Emphasized quote variant. |
| `core/verse` | Monospace poetry block. |
| `core/table` | Static table — no data source required. |

### Media

| Block | Notes |
| --- | --- |
| `core/image` | M4 media bridge wires `MediaUpload` via `@wordpress/hooks`. |
| `core/gallery` | Uses the same media bridge. |
| `core/video` | URL / upload. |
| `core/audio` | URL / upload. |
| `core/file` | URL / upload. |
| `core/embed` | oEmbed providers resolved client-side. |
| `core/cover` | Background image + nested blocks. |
| `core/media-text` | Side-by-side media + text. |

### Layout

| Block | Notes |
| --- | --- |
| `core/columns` | Multi-column container. |
| `core/group` | Generic container. |
| `core/row` | Variation of `core/group` (listed for clarity — no separate registration). |
| `core/stack` | Variation of `core/group` (listed for clarity — no separate registration). |
| `core/buttons` | Button row container. |
| `core/separator` | Horizontal rule. |
| `core/spacer` | Vertical whitespace. |
| `core/details` | Disclosure element. |

### Simple widgets

| Block | Notes |
| --- | --- |
| `core/search` | Static form — posts target via host app. |
| `core/latest-posts` | **Stubbed.** Renders an empty list against the core-data shim; flagged here so the UX expectation is explicit — the block is still usable as a placeholder authors can drop in before wiring a real data source. |

## Empty-state — disabled by default

These blocks do not crash, but the shim returns empty data for the selectors
they depend on, so the block renders a placeholder, empty list, or blank
title. Better UX is no block at all until `cms-framework` lands.

| Block | Required data |
| --- | --- |
| `core/latest-comments` | Comments feed. |
| `core/archives` | Archive list query. |
| `core/categories` | Term hierarchy. |
| `core/tag-cloud` | Term weights. |
| `core/rss` | External feed fetch — not blocked by shim but disabled for consistency with the other feed widgets. |
| `core/calendar` | Post date aggregation. |
| `core/page-list` | Hierarchical page query. |
| `core/table-of-contents` | Heading scan against rendered entity. |

## Crash or backend-required — disabled by default

These blocks call selectors the shim returns `null` for but which the block's
own edit component assumes are non-null, or they rely on a resolver the shim
does not implement. They are permanently in the deny-list until a real
`core` store replaces the shim.

### Site / theme blocks

- `core/navigation`
- `core/navigation-link`
- `core/navigation-submenu`
- `core/navigation-overlay-close`
- `core/home-link`
- `core/site-logo`
- `core/site-title`
- `core/site-tagline`
- `core/template-part`
- `core/breadcrumbs`
- `core/page-list-item`
- `core/loginout`

### Query loop

- `core/query`
- `core/query-loop` (listed per spec; no separate upstream block — the block
name is `core/query` and the loop is driven by `core/post-template`).
- `core/query-pagination`
- `core/query-pagination-next`
- `core/query-pagination-numbers`
- `core/query-pagination-previous`
- `core/query-no-results`
- `core/query-title`
- `core/query-total`
- `core/post-template`
- `core/read-more`

### Post context

- `core/post-title`
- `core/post-content`
- `core/post-excerpt`
- `core/post-date`
- `core/post-author`
- `core/post-author-name`
- `core/post-author-biography`
- `core/post-featured-image`
- `core/post-navigation-link`
- `core/post-terms`
- `core/post-time-to-read`
- `core/post-comments-form`
- `core/post-comments-count`
- `core/post-comments-link`
- `core/post-comment`

### Comments

- `core/comments`
- `core/comment-author-avatar`
- `core/comment-author-name`
- `core/comment-content`
- `core/comment-date`
- `core/comment-edit-link`
- `core/comment-reply-link`
- `core/comment-template`
- `core/comments-title`
- `core/comments-pagination`
- `core/comments-pagination-next`
- `core/comments-pagination-numbers`
- `core/comments-pagination-previous`
- `core/avatar`

### Taxonomy

- `core/term-count`
- `core/term-description`
- `core/term-name`
- `core/terms-query`
- `core/term-template`

### Internal / developer-only

- `core/missing` (fallback renderer for unknown blocks; internal)
- `core/html` (raw HTML — security review before enabling)
- `core/shortcode` (WordPress-only)
- `core/freeform` (TinyMCE classic block)
- `core/pattern` (pattern insertion — needs pattern registry)
- `core/block` (reusable block — needs entity store)
- `core/footnotes` (needs meta storage)
- `core/more` (classic read-more — post-context only)
- `core/nextpage` (pagination break — post-context only)
- `core/social-links`
- `core/social-link`
- `core/text-columns` (deprecated upstream)
- `core/icon`
- `core/math`

## Experimental / opt-in blocks

These blocks only register when a window flag is set
(`window.__experimentalEnableFormBlocks`,
`window.__experimentalEnableBlockExperiments`). The editor does not set any
of these flags, so they are effectively disabled regardless of config.

- `core/form`, `core/form-input`, `core/form-submit-button`, `core/form-submission-notification`
- `core/tab`, `core/tabs`, `core/tabs-menu`, `core/tabs-menu-item`, `core/tab-panel`
- `core/playlist`, `core/playlist-track`
- `core/accordion`, `core/accordion-item`, `core/accordion-heading`, `core/accordion-panel`

## How the allow-list + deny-list resolves

`enabled_blocks` is an allow-list: when non-empty, only the listed names are
exposed to the inserter. `disabled_blocks` is an always-applied deny-list;
even if a block appears on the allow-list, the deny-list removes it.

The frozen defaults ship with `enabled_blocks` populated — so new blocks
introduced by a future `@wordpress/block-library` upgrade are **implicitly
disabled** until explicitly added to the allow-list. That is intentional:
users should never be surprised by a new block that the shim hasn't been
validated against.
81 changes: 81 additions & 0 deletions src/VisualEditor.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,85 @@ public function getRegistry(): BlockTypeRegistry
{
return $this->registry;
}

/**
* Returns the fully-qualified names of blocks that should be exposed to
* the editor after the allow-list + deny-list filters run.
*
* Resolution order:
* 1. Start with the configured `enabled_blocks` allow-list. When
* empty, fall back to every block currently in the registry — the
* allow-list is only enforced when the host app has opted in.
* 2. Remove anything in the `disabled_blocks` deny-list.
* 3. De-duplicate and preserve authoring order.
*
* The return value is deterministic (no registry lookups, no locale
* sorting) so it can drive a snapshot test.
*
* @since 1.0.0
*
* @return array<int, string>
*/
public function getEnabledBlockNames(): array
{
$enabled = $this->stringListFromConfig( 'artisanpack.visual-editor.enabled_blocks' );
$disabled = $this->stringListFromConfig( 'artisanpack.visual-editor.disabled_blocks' );

$candidates = [] === $enabled
? array_column( $this->registry->all(), 'name' )
: $enabled;

$denyIndex = array_flip( $disabled );
$seen = [];
$result = [];

foreach ( $candidates as $name ) {
if ( ! is_string( $name ) ) {
continue;
}

$normalized = trim( $name );

if ( '' === $normalized || isset( $denyIndex[ $normalized ] ) || isset( $seen[ $normalized ] ) ) {
continue;
}

$seen[ $normalized ] = true;
$result[] = $normalized;
}

return $result;
}
Comment on lines +113 to +142

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
# Verify call sites and current blocks API behavior.
rg -nP --type=php '\bgetEnabledBlockNames\s*\(' -C2
rg -nP --type=php 'class\s+VisualEditorBlocksController|function\s+index\s*\(|registry->all\s*\(' -C3

Repository: ArtisanPack-UI/visual-editor

Length of output: 5295


🏁 Script executed:

#!/bin/bash
# Check if VisualEditor is used in the controller
cat -n src/Http/Controllers/VisualEditorBlocksController.php

# Search for integration/feature tests of blocks endpoint
find . -type f -name "*.php" -path "*/tests/*" | xargs grep -l "VisualEditorBlocksController\|blocks.*endpoint\|/blocks\|getEnabledBlockNames" | head -10

# Check if there are any route tests
rg -nP --type=php "VisualEditorBlocksController|route.*blocks" tests/

Repository: ArtisanPack-UI/visual-editor

Length of output: 1376


🏁 Script executed:

#!/bin/bash
# Check the blocks controller test
cat -n ./tests/Feature/VisualEditor/BlocksControllerTest.php

# Also check if there are any recent commits about filtering
git log --oneline -n 20 --all -- src/Http/Controllers/VisualEditorBlocksController.php 2>/dev/null | head -5

Repository: ArtisanPack-UI/visual-editor

Length of output: 1330


🏁 Script executed:

#!/bin/bash
# Check the git commit about freezing the allow-list
git show 29a0d18 --stat 2>/dev/null | head -20

# Check if there are config files for enabled/disabled blocks
rg -P --type=php 'enabled_blocks|disabled_blocks' -C2

# Check if there's documentation about block filtering
find . -type f \( -name "*.md" -o -name "*.php" \) | xargs grep -l "enabled_blocks\|disabled_blocks\|getEnabledBlockNames" | head -10

Repository: ArtisanPack-UI/visual-editor

Length of output: 5674


The controller still returns all blocks unfiltered, bypassing the frozen V1 allow-list.

getEnabledBlockNames() was added (commit 29a0d18) but VisualEditorBlocksController::index() never integrated it. The endpoint at line 38 (src/Http/Controllers/VisualEditorBlocksController.php) returns $this->registry->all() unchanged, meaning the configured enabled_blocks and disabled_blocks from config/visual-editor.php are ignored.

Wiring required
+ use ArtisanPackUI\VisualEditor\VisualEditor;
+
  public function __construct(
      protected BlockTypeRegistry $registry,
+     protected VisualEditor $editor,
  ) {
  }

  public function index(): JsonResponse
  {
+     $enabled = array_flip( $this->editor->getEnabledBlockNames() );
+     $blocks  = array_values( array_filter(
+         $this->registry->all(),
+         static fn ( array $block ): bool => isset( $enabled[ $block['name'] ?? '' ] )
+     ) );
-     return response()->json( ['blocks' => $this->registry->all()] );
+     return response()->json( ['blocks' => $blocks] );
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/VisualEditor.php` around lines 113 - 142, The controller currently
returns the full registry instead of honoring the VisualEditor allow/deny lists;
update VisualEditorBlocksController::index() to call
VisualEditor::getEnabledBlockNames() and use that list to filter the registry
results before returning them (e.g. map the enabled names to the corresponding
entries from $this->registry->all()); ensure you reference the
getEnabledBlockNames() method on the VisualEditor instance (or injected service)
and use the registry->all() payload to pick only entries whose 'name' matches
the enabled list so enabled_blocks/disabled_blocks in config are respected.


/**
* Pulls a config key, coerces it to a list of trimmed non-empty strings.
*
* @since 1.0.0
*
* @return array<int, string>
*/
protected function stringListFromConfig( string $key ): array
{
$raw = config( $key, [] );

if ( ! is_array( $raw ) ) {
return [];
}

$out = [];

foreach ( $raw as $value ) {
if ( ! is_string( $value ) ) {
continue;
}

$trimmed = trim( $value );

if ( '' !== $trimmed ) {
$out[] = $trimmed;
}
}

return $out;
}
}
Loading