@wulperstd/renderer-html
v1.4.1
Published
DOM-free string-based HTML renderer for the block editor's document JSON.
Readme
@wulperstd/renderer-html
DOM-free, string-based HTML renderer for the block editor's document JSON. It
turns validated (or unvalidated) block document JSON into an HTML string,
without ever constructing a real DOM, without depending on jsdom/happy-dom,
and without requiring document/window to exist in the running process.
See openspec/changes/static-html-renderer/design.md for the full design
rationale (escaping, scheme enforcement, placeholders, subsumption).
Public API
import {
renderToHtml,
renderToHtmlWithDiagnostics,
escapeHtmlText,
escapeHtmlAttribute,
} from '@wulperstd/renderer-html';renderToHtml(doc: unknown): string
Renders a block document to an HTML string. Never throws and never returns an empty string for non-empty input. Unrecognised or invalid content is rendered as a visible placeholder (see below) rather than silently dropped.
renderToHtml({
type: 'doc',
attrs: { schemaVersion: 1 },
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Hello' }] }],
});
// => '<p>Hello</p>'renderToHtmlWithDiagnostics(doc: unknown): { html: string; placeholders: readonly PlaceholderRecord[] }
Same rendering behaviour as renderToHtml, but also returns the list of every
placeholder that was emitted, each carrying the RFC 6901 JSON pointer path into
the input document where it occurred, its kind, and the declared type (or
null when the type itself could not be determined).
const { html, placeholders } = renderToHtmlWithDiagnostics(doc);escapeHtmlText(value: string): string / escapeHtmlAttribute(value: string): string
The renderer's own escaping primitives, exported for reuse by any caller that needs to interpolate raw text or attribute values consistently with the renderer's own output.
Placeholder kinds
The renderer inspects each node and mark directly — it never calls schema's
validateReport() and behaves identically whether or not a prior validation
step has run. When it encounters unrecognised or invalid content, it emits a
visible placeholder instead of silently omitting or misrendering it:
| Kind | When it occurs | Shape |
| --------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| unknown-node | A node type outside the renderer's known set (paragraph, heading) | <span data-wsb-placeholder="unknown-node" data-wsb-type="{type}">[unsupported node: {type}]</span> |
| unknown-mark | A mark type outside the renderer's known set (bold, italic, link) | Same shape, kind="unknown-mark" |
| invalid-attrs | A known node/mark whose attributes fail the renderer's own defensive check (e.g. heading.level outside 1–3) | Same shape, kind="invalid-attrs" |
| malformed | A node/mark that is not a plain object, is missing a string type, or (for a text node) is missing a string text | Same shape, kind="malformed" |
| unsafe-href | A link mark whose href is rejected by schema's isSafeHref predicate | <span data-wsb-placeholder="unsafe-href" data-wsb-type="link">{escaped children}</span> — the anchor is dropped, the text content is kept |
unsafe-href is the one exception to the generic [unsupported node: {type}]
text: per design.md Decision E, its placeholder wraps the mark's own (already
escaped) children instead of replacing them, because the safety failure is on
the link's destination, not on the text it wraps.
A placeholder never removes valid sibling content: unrecognised or invalid content is replaced in its own position, and the rest of the document renders normally around it.
Render-time link scheme enforcement
Every URL-bearing attribute the renderer can emit (today, only link.href) is
routed through schema's exported isSafeHref predicate before being
escaped and emitted — the same predicate @wulperstd/schema's own
link.attrs.href validation rule uses at write time, so render-time and
write-time enforcement cannot drift apart. A rejected href never reaches the
output as a live, navigable link; the unsafe-href placeholder is emitted in
its place.
DOM-free guarantee
This package is enforced to be import-graph DOM-free by the
renderer-dom-free dependency-cruiser rule (see .dependency-cruiser.cjs and
tests/boundaries/boundaries.test.ts), and its built dist/ output is proven
DOM-free in a bare Node.js process by scripts/check-dist-node-safety.mjs
(pnpm run check:dist-node-safety).
