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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- Clarify native intent styling versus HTML/CSS conversion, with runnable native-attribute, headerless-table and sequential library examples. Verified theme layout contracts can override generic block-mapping defaults.
- Warn when the native block constructor discards explicitly supplied intent attributes, including the attribute key and input path without raw values. Warnings remain informational in strict mode.
- Preserve nested lists inside intent list items.
- Reject malformed intent nodes, children, list items and table cells with input-located errors instead of returning partial content.
Expand Down
24 changes: 24 additions & 0 deletions dev/test/intent.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import path from 'node:path';
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { describe, expect, it, vi } from 'vitest';
import { assemble, realize, validate } from '../../src/index.js';
Expand All @@ -9,6 +10,29 @@ import type { IntentNode } from '../../src/types.js';
const FIXTURES = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures');

describe('intent assembly', () => {
it('preserves the native styles and headerless table taught in the shipped guide', async () => {
const guide = await readFile(new URL('../../skills/block-runner/references/ASSEMBLE.md', import.meta.url), 'utf8');
const example = guide.match(/### Native styles and layout[\s\S]*?```json\n([\s\S]*?)\n```/);
expect(example).not.toBeNull();
const report = await realize(example![1]);
expect(report.ok).toBe(true);
expect(report.summary.warnings).toBe(0);
const wp = await getWp();
const [group] = wp.parse(report.output!);
expect(group.attributes.layout).toEqual({ type: 'constrained' });
const [paragraph, table] = group.innerBlocks;
expect(paragraph.attributes).toMatchObject({
textColor: 'contrast', backgroundColor: 'base', className: 'is-style-callout',
style: {
typography: { textAlign: 'center' },
spacing: { padding: { top: 'var:preset|spacing|40', bottom: 'var:preset|spacing|40' } },
},
});
expect(table.attributes.head).toEqual([]);
expect(report.output).not.toContain('<thead>');
expect(report.output).toContain('<td>Name</td><td>Value</td>');
});

it.each([false, true])('warns for discarded explicit attributes without failing (strict: %s)', async (strict) => {
const report = await realize(JSON.stringify({ blocks: [{
block: 'core/group', children: [{
Expand Down
26 changes: 26 additions & 0 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,32 @@ const fixed = await canonicalize(converted.output);

`realize(json)` consumes an intent JSON string and returns the complete assembly report, including media/token processing and validation. CLI `assemble` calls it. Library `assemble(nodes)` only builds Gutenberg block objects; it does not run that finalisation workflow.

Intent assembly accepts explicit native attributes, including theme presets, spacing, layout and
block style classes. It does not extract those attributes from CSS; use `convert` for existing
authored HTML/CSS requiring interpretation. See the [native styling and headerless-table example](../skills/block-runner/references/ASSEMBLE.md#native-styles-and-layout).
Its shapes are checked against the pinned headless registry; target-editor and theme rendering
remain separate proof.

For several inputs, call `realize()` sequentially in one process and keep each report:

```js
import { realize } from 'block-runner';

const inputs = [
{ name: 'intro', blocks: [{ block: 'core/paragraph', text: 'Welcome' }] },
{ name: 'details', blocks: [{ block: 'core/list', items: ['First', 'Second'] }] },
];
const reports = [];
for (const { name, blocks } of inputs) {
const report = await realize(JSON.stringify({ blocks }), { sourcePath: `${name}.json` });
reports.push({ name, report });
}
console.log(reports.map(({ name, report }) => ({ name, ok: report.ok, ...report.summary })));
```

Check each report's `ok` and `items` before using its output. Untouched successful output has
already passed media/token finalization and validation; another validation call adds no proof.

### Registered-block authoring contract

`GeneratedAuthoringPlan` is the public authoring contract at the preview/confirmation/write
Expand Down
50 changes: 48 additions & 2 deletions skills/block-runner/references/ASSEMBLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,51 @@ and styles nothing. That last one still passes `validate`, because the class agr
attribute it was built from. Read the theme's palette before you choose, and prefer the slug when
one fits.

### Native styles and layout

Use native attributes directly when authoring a styled page. Alignment belongs at
`attrs.style.typography.textAlign` in the pinned registry, and layout belongs at `attrs.layout`.
A top-level intent `layout` is not mapped; a paragraph's flat `attrs.textAlign` is discarded.
This example also shows a headerless table:

```json
{
"blocks": [{
"block": "core/group",
"attrs": { "layout": { "type": "constrained" } },
"children": [{
"block": "core/paragraph",
"text": "Native styling",
"attrs": {
"textColor": "contrast",
"backgroundColor": "base",
"className": "is-style-callout",
"style": {
"typography": { "textAlign": "center" },
"spacing": { "padding": { "top": "var:preset|spacing|40", "bottom": "var:preset|spacing|40" } }
}
}
}, {
"block": "core/table",
"attrs": { "body": [{ "cells": [{ "content": "Name", "tag": "td" }, { "content": "Value", "tag": "td" }] }] }
}]
}]
}
```

Save this as `intent.json` and run `assemble intent.json --json`, or pass its JSON string to
library `realize()`. The preset slugs `contrast`, `base`, `40` and class `is-style-callout` are
illustrative: use values and registered block styles verified in the target theme. Accepting a
class or preset does not prove that the theme defines it.

For a headerless table, use native `attrs.body` cells as shown. The `rows` string-array shorthand
is still useful when a header is wanted: its first row becomes the header and later rows become
the body. Do not remove generated `<thead>` markup to change that contract.

These attribute shapes are checked against Block Runner's pinned Gutenberg packages, not every
WordPress version. Verify the target editor, save/reopen and frontend before claiming matching
presentation. Native attributes do not ask Block Runner to interpret arbitrary source CSS.

## Available blocks

`core/cover`, `core/columns`, `core/column`, `core/media-text`, `core/group`, `core/heading`,
Expand Down Expand Up @@ -104,8 +149,9 @@ Keep every real nesting level.
when the background is a solid colour or gradient rather than an image. If the hero sets
copy beside a product image, that is `core/columns` *inside* the cover, with the image as a
`core/image` in a `core/column` — not `core/media-text`.
- **Image beside text as a mid-page feature row** → a `core/media-text` (image on the media
side, heading/paragraph/list/buttons on the text side). Never `core/columns` for this. Where
- **Image beside text as a mid-page feature row** → prefer `core/media-text` (image on the media
side, heading/paragraph/list/buttons on the text side). A verified theme or project layout
contract can call for `core/columns`; without that evidence, keep the Media & Text default. Where
several such rows alternate down the page, they are sibling `core/media-text` blocks sharing
the one section group — do not give each row a group of its own.
- **FAQ / accordion (a SET of collapsible panels)** → `core/group` of a heading then ONE
Expand Down
17 changes: 10 additions & 7 deletions skills/block-runner/references/GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ to adding a plugin merely because it is easier to generate. The handoff is descr
| You need | Use | Why |
|---|---|---|
| A reusable named block that must live in plugin or theme source | **`author preview`** → confirmation → **`author write`**, then package and proof it | This produces registered-block source. It is not page `post_content`. |
| New content for a page or post, with no authored HTML | **`assemble`** | An intent tree becomes native page blocks. |
| Existing authored HTML that must become page or post `post_content` | **`convert`** | Rule-based translation of existing markup, and the only content path that carries CSS. Do not use frontend-scraped render output. |
| New content for a page or post, including explicit native styles and theme presets | **`assemble`** | An intent tree becomes native page blocks with the attributes you supply. |
| Existing authored HTML/CSS that must become page or post `post_content` | **`convert`** | Rule-based interpretation of authored markup and supported CSS. Do not use frontend-scraped render output. |
| Block markup you already produced, before saving it to WordPress | **`validate`** → **`fix`** → **`validate`** | Checks block markup; site references and editor behavior still need target-site verification. |

Choose by the requested artifact, not merely by the input format. A supplied HTML design still
Expand All @@ -117,11 +117,14 @@ The single most common mistake is reaching for `convert` when you were about to
HTML yourself. If you are the one inventing the structure, do not write HTML and convert it —
describe the structure to `assemble` directly. You will get better blocks with less work.

**The exception that matters:** if the input has meaningful CSS you need to preserve —
brand colours, custom spacing, a specific look — use `convert`, not `assemble`. An intent
tree carries structure and content, not styling, so `assemble` will produce clean but plainer
blocks. `convert --styling relaxed` keeps exact off-theme values on the block. If you are
unsure whether the styling matters, ask the user rather than silently flattening their design.
Intent trees carry explicit native attributes, including theme colour presets, spacing, layout
and block style classes. Author those directly when you know the native shape; do not create
HTML just to convert it back. See the [native styling example](ASSEMBLE.md#native-styles-and-layout).

Use `convert` when existing authored HTML/CSS needs interpretation. `assemble` does not infer
attributes from source CSS, and `--styling` remains a conversion option. Neither route proves
that a target theme renders the intended design: inspect its tokens and conventions, then check
the actual editor and frontend when that proof is in scope.

---

Expand Down
Loading