crypto.randomUUID exists only in a secure context, so an admin served
over plain HTTP from a LAN host threw on every editor upload while the
status line still said uploading. uploadRequestId falls back to a v4
UUID from crypto.getRandomValues, or a time and counter id when no
crypto object exists.
captureAttributes and renderCaptured dropped only on* names, so id,
name, style, srcset, background and the like from stored HTML were
rendered by ProseMirror into the live admin DOM (DOM clobbering of
document.querySelector, fixed-position overlays, remote loads). htmlAttrs
now keeps an allowlist: class, title, alt, width, height, colspan,
rowspan, start, rel, target and data-*; href and src stay with the link
and image nodes. Other attributes are dropped from a block once it is
edited; untouched documents and untouched blocks keep their bytes.
- schema: every raw HTML tag rule, figure shapes, span mark, unmodelled
attributes, handler attributes dropped, links, tables, no trailing node
- serializer: block reuse after an edit, a deletion and an insertion,
separators and comments kept, captureBlocks fallbacks
- embeds: parseEmbedUrl table, placeholder round trip, isPolicyUrl table,
upload id matching and timeout, labels, disabled and invalid
- element.test.ts: an untouched value survives Source tab, tab buttons,
preview and reattach byte for byte with no summer-change
- TestBlogEditorLoad: HTML verbatim, markdown as content_html, FormatHTML
fallback, nil and empty values
- TestPostsAdminResaveKeepsHTML (Docker host): GET then PUT the served
values for an HTML and a markdown post, exact bytes, no notices, then a
clean edited save in both locales
- The preview re-renders 300 ms after the last WYSIWYG edit, Source input or external value, never while off or after detach.
- YouTube placeholders show as img.youtube.com thumbnail cards with no player; the preview copy drops active elements and on* attributes, and its stylesheet uses colours resolved from the host's --c-* values.
- A per-element Preview toggle (aria-pressed, off by default) splits the editor 50/50 with an auto-fit grid that wraps the pane below on narrow widths.
- The pane is an iframe with an empty sandbox, content set only through srcdoc, created on first use; it reads only the value getter, so previewing never edits or emits summer-change.
- preservationExtensions: raw HTML cards for unmodelled blocks (shown as
text), figure with figcaption, the YouTube embed placeholder node, span
marks, a global attribute map (never on* handlers), tables without
colgroup or min-width, inline images without base64
- serializeDocument keeps the original bytes of every top-level block still
equal to the loaded one, so editing one paragraph changes only that
paragraph; tables serialize with thead and bare cell text
- Links accept only http, https, mailto and relative URLs (isPolicyUrl, the
rule both sanitizer policies apply) and gain no target, rel or class
- Toolbar with headings, marks, lists, links, blockquote, table editing,
images, embeds, raw HTML block, undo and redo; en and pl labels
- Images from the picker or a drop go out as summer-upload and are inserted
on summer-upload-result with alt text and an optional caption
- editor-corpus.json: the editor own output, rebuilt and checked by
corpus.test.ts (UPDATE_CORPUS=1 rewrites it) for the policy tests
- README Blog Editor section
- golem15-journal-blogeditor (BlogEditorElement): light-DOM element with a
TipTap WYSIWYG tab and a Source tab that edits the exact HTML string
- The value is the exact served string until a user edit; external values
go in with emitUpdate false; summer-change fires on user edits only and
an echo set is a no-op
- StarterKit without the trailing node, links without target, rel or class;
injectCSS false with the ProseMirror base rules in blogeditor.css under
the element tag, colours from --c-* only
- Committed bundle embedded through admin.go; vitest smoke on an imported
post fixture (footer, style block, spans, figure, table, entities)