Changelog
Published releases, newest first. Read from the GitHub releases the release workflow writes.
Published releases, newest first. Read from the GitHub releases the release workflow writes.
dfbc49d: Stop the chrome throwing when an extension it offers is not loaded.
isAvailable asked editor.can().toggleHighlight(), but editor.can() only
carries the commands the configured extensions registered. With
preset="minimal" — no highlight, no tables — selecting text threw
editor.can(...).toggleHighlight is not a function while the toolbar was
rendering, and the cell menu threw the same way on mergeCells, so the whole
editor failed to mount. The same held for any host passing its own
extensions array without one of those types.
Every default action now checks its mark or node is in the schema before
asking whether the command applies, through a shared whenMarkExists /
whenNodeExists guard. A type the editor does not have reads as unavailable
and the button is hidden, which is what isAvailable was for.
f254f17: Fix task list items rendering stacked instead of inline, and list markers disappearing under a CSS reset.
The task item rules were keyed to li[data-type='taskItem'], an attribute
that only exists in serialised output. Tiptap builds the live item in
addNodeView from the node's own attributes — data-checked, data-id — and
never applies the data-type that renderHTML merges in. So the selector
styled every rendered page and no actual editor, and the checkbox sat above
its label rather than beside it for as long as task lists have existed. The
rules now match through the parent list, ul[data-type='taskList'] > li,
which is present in both.
list-style is now stated rather than inherited from the user agent. Every
common reset strips list markers, so bullets and numbers vanished in any app
using Tailwind's preflight or normalize — and "works without a CSS framework"
should not mean "breaks with one".
Both were found by putting a task list and a bullet list in the documentation site's live demo.
5da026d: Add @blockleaf/renderer (RD-01) — a new package that turns a stored document
into React elements on a server, without the editor and without ProseMirror.
b6f7882: Add the upload adapter interface (MC-01). Hosts implement UploadAdapter and
the editor drives it; no storage provider is baked in. The second argument is a
context object rather than a bare AbortSignal so adapters can report
determinate progress, and isUploadResult validates what an adapter resolves
with before it can reach the document.
05657cb: Add drop, paste and picker entry points for uploads (MC-02). Dropping files on
the editor, pasting them from the clipboard, or calling openUploadPicker
screens each file for type and size before it leaves the browser, then
hands it to the adapter and inserts an image where it landed.
The in-flight placeholder is a widget decoration rather than a node, so ProseMirror maps its position through every edit made while the upload runs — an image dropped at the end of a paragraph still lands there after typing above it — and nothing has to be stripped out of the document before saving.
upload, uploadAccept, uploadMaxBytes and onUploadError are new options
on useBlockleafEditor and <BlockleafEditor>. Without an adapter the
extension is not added at all, so files fall through to the browser rather
than being swallowed.
5fa4e4c: Add upload progress, cancel and retry (MC-03). The in-flight placeholder now
shows a progress bar driven by the adapter's onProgress, with a Cancel
button that aborts the request through the signal.
Progress is indeterminate until an adapter reports a fraction — fetch cannot
measure upload progress, so a bar claiming a percentage it does not have would
be a lie. An indeterminate progressbar omits aria-valuenow rather than
reporting zero.
A failed upload no longer disappears. Its placeholder stays, holding the position it has been mapping all along, and offers Retry and Dismiss — so a retry puts the image where the user dropped it, even after editing above it. A cancelled upload is not reported as a failure.
a114e5d: Add the image block with alt text and alignment (MC-04). The node is
hand-rolled rather than taken from @tiptap/extension-image, and src is
sanitised in the schema: https:, relative paths and raster data URLs are
allowed, while javascript:, blob:, http: and SVG data URLs are rejected.
An unsafe <img> arriving through a paste is dropped rather than stored with
a null source.
Selecting an image opens a toolbar for alignment, alt text and delete. An empty alt is kept rather than discarded, since that is how an image is marked decorative.
import { RenderDocument } from '@blockleaf/renderer/react';
<RenderDocument doc={article.body} />;It has no runtime dependencies and React is an optional peer, so the
sanitisers at the root entry work in a plain Node script. RenderDocument
measures 1.35 kB.
BlockleafDocument satisfies the doc prop structurally — there is no import
from @blockleaf/editor and nothing to convert.
Every element matches what the editor writes for the same node, so one
stylesheet fits both; a parity suite renders the same document through both
packages and fails if they diverge. Three differences are deliberate: the task
checkbox is disabled, since nothing here can write a change back; an unknown
node renders its children rather than throwing, because a document written by
a newer editor must not take down the page; and src and href are
sanitised again at render time, because stored JSON predates any guarantee the
editor made about it.
6d2770d: Add per-node and per-mark component overrides (RD-02).
<RenderDocument
doc={article.body}
components={{ image: MyImage, codeBlock: MyCode }}
/>An override is a patch over the defaults, not a replacement mapping — naming
image does not mean re-implementing paragraphs. A name the defaults do not
know works too, which is how a host renders a node type of its own.
The attribute helpers the default components use are exported alongside, so an
override routing images through next/image can call imageAttributes(node)
and get a sanitised src rather than re-deriving the rule.
Overrides are merged per render rather than memoised, because the component has to work in a server component, where hooks do not exist. Hoisting the override map to module scope avoids the merge.
a9c4af8: Add the HTML-string and plain-text exports (RD-03).
import { renderToHTML } from '@blockleaf/renderer/html';
import { extractHeadings, toPlainText } from '@blockleaf/renderer';
const body = renderToHTML(doc); // emails, RSS, OG images
const excerpt = toPlainText(doc).slice(0, 200);
const toc = extractHeadings(doc);renderToHTML measures 1.5 kB and needs no React at all — the ./html entry
imports nothing. It takes the same components and marks overrides the
React target does, and writes the same markup: the parity suite now holds
three implementations to each other, the two targets and the editor itself.
Escaping is this target's own job rather than React's, so text and attribute
values are escaped separately, and a document whose stored alt is
" onerror="alert(1) renders as an attribute rather than as a handler.
toPlainText takes a blockSeparator and leaves image alt text out unless
asked, since alt text reads as an interruption in an excerpt and as useful
signal in a search index. extractHeadings returns level, text and block id
in document order.
b358752: Render tables as real table markup, in both targets (RD-07).
prosemirror-tables has no concept of a table section, so the editor writes
every row into <tbody>, header cells included. That is tolerable in a
surface the user is looking at and not tolerable in output: <thead> is most
of what an email client, an RSS reader or a screen reader has to go on.
So a leading row made entirely of header cells becomes <thead>, and the rest
<tbody>. A row with a mix of headers and cells is a header column, which
prosemirror-tables also allows, and is left in <tbody> rather than claiming
to label everything below it. Header cells carry scope: col in a header
row, row for a header cell leading a body row.
Stored column widths move into <colgroup>, where the editor also puts them.
The colwidth attribute the editor additionally writes on each cell is
dropped — it is not an HTML attribute, and the width is already in the
<col>.
This is the only node the renderer deliberately writes differently from the editor. Everything else is held to it by the parity suite.
The bubble menu's link editor is now one of a general panel slot, so the
toolbar's keyboard handling lives in one place. Three CSS classes were
renamed to match: blockleaf-bubble-menu__link-editor, __link-input and
__link-error are now __panel, __panel-input and __panel-error.
Closing a panel now returns focus to the button that opened it, which the
link editor had never managed.
c004dfe: Add the syntax-highlighted code block with a language picker (MC-05), as the
new opt-in subpath @blockleaf/editor/code-block.
import { createExtensions } from '@blockleaf/editor/extensions';
import { withCodeBlock } from '@blockleaf/editor/code-block';
const extensions = withCodeBlock(createExtensions('standard'));It is a subpath rather than part of the default build because lowlight and
its grammars cost 16.53 kB and the default bundle had 15.37 kB left — shipping
it by default put the editor at 161.16 kB, over the ceiling. Importing nothing
costs nothing, and the main entry is unchanged at 144.63 kB. See F14.
Seven grammars ship by default — bash, css, javascript, json, python,
typescript and xml — and hosts register more through additionalLanguages.
StarterKit's plain code block is untouched for anyone who does not import
the subpath, and the stored shape is identical either way.
A code block whose language this host has not registered now renders
unhighlighted instead of throwing during editor construction, and keeps its
language attribute rather than having it stripped on the next save.
@tiptap/extension-drag-handle, which imports yjs at module scope and costs 32.87 kB; this costs 1.83 kB and no new dependency.declares nosideEffects, which pushed the schema-only entry from 136 B to 60.77 kB. StarterKit's trailingNode` no longer appends an empty paragraph after a heading, and a saved document that needs a trailing paragraph gets one on load without reporting it as a user edit.label prop giving the editing surface an accessible name, Shift+F10 and Mod+Alt+M for the block menu, Mod+Alt+R / Mod+Alt+C for the table menus, and fixes a colour-contrast failure on selected menu rows. The link editor now wins focus reliably, including when Mod+K is pressed before the toolbar has appeared.insertTable command from EC-13. Slash commands can carry a picker for any command that needs an argument.shouldShow, offering merge, split and structure operations for a selection spanning cells. The text bubble menu stands down for a cell selection so the two never compete.pnpm dev leaving the sandbox unable to resolve @blockleaf/editor. The stylesheet is now copied by the bundler rather than the build script, so watch mode emits it too, and clean no longer wipes dist/ when the watcher starts. The published ./styles.css subpath is unchanged.@blockleaf/editor/validate