From 5ba8ffbb057779056bb3dc03ea3825ce6a11f3d1 Mon Sep 17 00:00:00 2001 From: Noel Tock Date: Sun, 4 Oct 2026 08:54:00 +0400 Subject: [PATCH 1/2] docs: clarify native intent styling and library assembly --- CHANGELOG.md | 1 + docs/reference.md | 26 +++++++++++ skills/block-runner/references/ASSEMBLE.md | 50 +++++++++++++++++++++- skills/block-runner/references/GUIDE.md | 17 +++++--- 4 files changed, 85 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f5256a..58c7659 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/docs/reference.md b/docs/reference.md index 40fdb0c..ce7de3a 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -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 diff --git a/skills/block-runner/references/ASSEMBLE.md b/skills/block-runner/references/ASSEMBLE.md index 2e72fba..d1ed0b5 100644 --- a/skills/block-runner/references/ASSEMBLE.md +++ b/skills/block-runner/references/ASSEMBLE.md @@ -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 `` 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`, @@ -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 diff --git a/skills/block-runner/references/GUIDE.md b/skills/block-runner/references/GUIDE.md index 02c7fa5..65d1a57 100644 --- a/skills/block-runner/references/GUIDE.md +++ b/skills/block-runner/references/GUIDE.md @@ -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 @@ -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. --- From 0590aa5b0d0f33952cd4d8d226e92b86c7576d76 Mon Sep 17 00:00:00 2001 From: Noel Tock Date: Sun, 4 Oct 2026 08:54:00 +0400 Subject: [PATCH 2/2] test: execute native styling example from the guide --- dev/test/intent.test.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/dev/test/intent.test.ts b/dev/test/intent.test.ts index ba6b03e..fdcdc36 100644 --- a/dev/test/intent.test.ts +++ b/dev/test/intent.test.ts @@ -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'; @@ -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(''); + expect(report.output).toContain('NameValue'); + }); + 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: [{