Automattic / Automattic/blocks-engine

Model navigation once and project its unsupported residue generically

Open
#1,482 4 comments 0 reactions 0 assignees View on GitHub
Dominant language
PHP
Stars
14
Forks
2
Avg merge
2h 10m
Merged PRs (30d)
561

Description

## Problem

Navigation recognition, native mapping, and re-presentation are spread across four files with no shared model, and every source idiom that core blocks cannot serialize costs four separate additions: a new marker vocabulary, a new registry field, a new CSS emission site, and a new projector method.

Current cost, reproducible from the repo root:

```sh
# 16 distinct marker vocabularies
grep -rhoE "blocks-engine-[a-z-]*navigation[a-z-]*|blocks-engine-navigation-[a-z-]*" php-transformer/src/ \
| sed -E "s/-[0-9a-f]{8,}$//" | sort -u

# 3,592 lines across four navigation files
wc -l php-transformer/src/HtmlToBlocks/Patterns/NavigationPattern.php \
php-transformer/src/HtmlToBlocks/Style/NavigationStyleProjector.php \
php-transformer/src/HtmlToBlocks/NavigationBlockNormalizer.php \
php-transformer/src/HtmlToBlocks/Patterns/NavigationPatternContext.php
```

The vocabularies today: `brand-navigation-carrier`, `current-navigation-item`, `current-navigation-underline`, `direct-navigation`, `direct-navigation-link-color-*`, `direct-navigation-reset-*`, `inline-navigation`, `list-navigation`, `native-navigation-toggle-*`, `native-responsive-navigation`, `navigation-current-color-*`, `navigation-link-color-*`, `navigation-link-color-states-*`, `navigation-link-icon-*`, `navigation-submenu-background-*`, `projected-dialog-navigation`.

### The failure mode this shape produces

Because the pattern and the projector each independently re-derive the same fact, they can disagree — and the projector then discards the styling silently.

`NavigationStyleProjector::directNavigationSupportCss()` recomputes a hash the pattern already computed, and drops the rule when the class is absent:

```php
$expectedMarker = 'blocks-engine-direct-navigation-link-color-' . substr(hash('sha256', $safeColor), 0, 12);
$classes = preg_split('/\s+/', trim((string) ($attrs['className'] ?? '')));
if ( ! in_array($expectedMarker, $classes, true) ) {
continue;
}
```

`navigationLinkTextColorRules()` repeats the same shape with a second hash input (`$color . "\0" . $stateMask`). Any drift between the two derivations is an invisible loss of source presentation, which is a recurring source of "navigation still renders wrong" reports rather than a single bug.

## Expected contract

Introduce one navigation model and one projection mechanism.

1. **Recognition** produces a `NavigationModel` value object: landmark kind, items (label, url, current state, icon, colour states), overlay behaviour, toggle control, submenu containers, brand carrier.
2. **Native mapping** turns `NavigationModel` into core blocks (`core/navigation`, `core/navigation-link`, `core/navigation-submenu`). Pure, and emits no CSS.
3. **Residue** is one typed list of everything the model carries that core blocks cannot store: icon, per-state colour, submenu background, toggle box, spacing, current-item decoration.
4. **Projection** is a single generic emitter turning residue entries into `(marker class, declarations)` pairs through one registry.

Marker identity is produced once, by the model, and consumed by the emitter — so recognition and projection cannot disagree.

## Acceptance

- Adding a newly-discovered unsupported navigation idiom requires adding one residue type, with no new marker vocabulary, registry field, or emission site.
- Marker classes are generated in exactly one place and consumed in exactly one place.
- No projector recomputes a hash in order to match a class the pattern already assigned.
- The 292 parity fixtures pass unchanged, demonstrating this is a restructure and not a behaviour change.
- `NavigationStyleProjector` retains only navigation responsibilities.

## Related

- #1398 and #1161 are navigation capabilities that would each become a residue type or recognition rule under this model rather than new vocabularies.

## AI assistance disclosure

This issue was researched and written with AI assistance: Claude Sonnet 4.5 running in the opencode CLI agent. The AI enumerated the marker vocabularies and emission sites with the commands shown above, read the projector and pattern sources to identify the recompute-and-drop failure mode, and drafted the proposed contract. A human reviewed and directed the architectural framing.

Contributor guide

Open the contributing guide

Research direction

Start with NavigationPattern.php, NavigationPatternContext.php, NavigationBlockNormalizer.php, and NavigationStyleProjector.php, then run the two repository-root grep and wc commands to map the current vocabularies and file size. Use the 292 parity fixtures as the regression check; done means the model and generic residue projection satisfy the listed acceptance criteria without changing their behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
php
Domain
backend
Issue type
Refactor
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.