Skip to content

fix(super-editor): persist data-URI images set as SDT preset content (SD-3116)#3516

Merged
caio-pizzol merged 106 commits into
luccas/sd-3258-bug-image-resizing-inside-structured-contentfrom
luccas/sd-3116-bug-image-set-as-presetcontent-in-sdt-field-does-not-persist
May 27, 2026
Merged

fix(super-editor): persist data-URI images set as SDT preset content (SD-3116)#3516
caio-pizzol merged 106 commits into
luccas/sd-3258-bug-image-resizing-inside-structured-contentfrom
luccas/sd-3116-bug-image-set-as-presetcontent-in-sdt-field-does-not-persist

Conversation

@luccas-harbour
Copy link
Copy Markdown
Contributor

Summary

Images set as presetContent inside a structured-content (SDT) field – most commonly SVG signatures and other preset graphics – were being dropped on DOCX export. The root cause was a chain of assumptions about data-URI images that only held for inline-pasted, base64 PNG/JPEG content:

  • the renderer's image data-URL allowlist was hard-coded to base64, payloads, so URL-encoded SVGs (the format produced by most signature widgets) were rejected at paint time;
  • the registration plugin always routed data-URI sources through the canvas resize pipeline, which strips vector content and produced no usable file for SVGs;
  • the DOCX exporter looked up the image's package path with src.split('word/')[1], which returns undefined for a data: URI, so the relationship target was never written and the <a:blip> lost its image reference;
  • non-base64 payloads and field-annotation images had no shared validation, so the editor surface, the painter, and the exporter all disagreed about which data URIs were safe.

This PR consolidates the data-URI policy into shared/url-validation, threads it through the painter, importer, registration plugin, and DOCX exporter, and adds a roundtrip test covering preset-content insertion → paint → export → re-import.

Highlights

Shared data-URI policy (shared/url-validation)

  • New exports: getDataUriMetadata, tryDecodeDataUriText, isValidImageDataUrl, plus IMAGE_DATA_URL_MIME_TYPES and MAX_IMAGE_DATA_URL_LENGTH.
  • Base64 payloads remain allowed for every supported image MIME type. Non-base64 payloads are accepted only for image/svg+xml, and only when the percent-encoded text decodes successfully. 10 MB cap is enforced uniformly.

Painter (layout-engine/painters/dom)

  • Inline image and field-annotation image rendering both delegate to isValidImageDataUrl. Removed the local VALID_IMAGE_DATA_URL regex / MAX_DATA_URL_LENGTH constant.
  • New tests cover non-base64 SVG rendering on both code paths.

Image registration plugin (super-editor/extensions/image)

  • SVG data URIs with finite intrinsic sizes are now registered in place, skipping the canvas resize step entirely.
  • getDataUriDecodedByteLength enforces the upload byte cap before in-place registration (handles both base64 and percent-encoded payloads).
  • Media registration mirrors entries to the parent editor's media store so child editors don't lose images at the parent boundary.
  • Async path: SVGs under the size cap upload directly without canvas reprocessing.

DOCX exporter (v3/handlers/wp/helpers/decode-image-node-helpers.js)

  • New createMediaTargetForDataUri allocates a stable word/media/image-<hash>.<ext> package path for each data-URI source, caches the mapping per export (params.dataUriMediaTargets), and resolves rId collisions by appending a random suffix when two distinct sources hash to the same path.
  • Relationship lookup is unified across header/footer and document parts (resolveImageRelationshipId + getImageRelationshipLookup).
  • Field-annotation export now validates the data URI before writing a relationship, falling back to text rendering when invalid.
  • Warns (instead of silently dropping) when a media target can't be resolved.

pm-adapter

  • resolveNodeSdtMetadata is now generic in its override type; callers like fieldAnnotationNodeToRun get the precise metadata type without a cast.
  • Empty-inline-SDT placeholder emission is now scoped to structuredContent metadata only — other inline SDT variants no longer collapse into a placeholder when empty.

Importer/helpers cleanup

  • helpers.js#dataUriToArrayBuffer accepts URL-encoded SVG payloads.
  • image-dimensions.js reads SVG intrinsic dimensions from <svg width/height> attributes (base64 and percent-encoded).
  • Shared simpleStringHash / stableHexHash helpers moved to core/utilities/hash.js; documentCommentsImporter and handleBase64 now reuse them.
  • mediaHelpers.js centralizes MIME → extension mapping (getImageExtensionFromMimeType) and re-exports shared metadata helpers with the extension annotation.

Tests

  • sd-3116-structured-content-image-roundtrip.test.js — 453 LOC suite covering preset-content insertion, paint, save, and re-import for both base64 and percent-encoded SVG signatures plus PNG cases.
  • structured-content-commands.test.js — verifies insertStructuredContentBlock registers the preset image in media and rewrites src to a word/media/... path.
  • imageRegistrationPlugin.browser.test.js — in-place SVG registration, parent-store mirroring, and upload cap.
  • handleBase64.test.js, image-dimensions.test.js, mediaHelpers.test.js, helpers.test.js, decode-image-node-helpers.test.js — coverage for non-base64 SVG paths, malformed payloads, MIME normalization, and exporter target collisions.
  • painters/dom/src/index.test.ts — non-base64 SVG rendering for both image runs and field annotations.

Risk / compatibility

  • Renderer policy changes are additive (accepts more valid URIs, rejects fewer) for base64 PNG/JPEG/etc. — existing assets render the same.
  • The exporter previously emitted broken relationships for data-URI images; output now contains real media parts. Any consumer that relied on the broken behaviour (unlikely) would notice extra word/media/image-*.svg parts.
  • Per-conversation parent-editor media mirroring is opt-in via editor.options.parentEditor — unchanged for standalone editors.

Register data URI image sources as DOCX media parts during export so
the relationship target resolves correctly. Map the svg+xml MIME
subtype to a .svg extension when deriving the media filename.
…processing

Detect SVG data URIs with known finite sizes and register them in place
during browser-path image handling, skipping the canvas-based resize
pipeline that strips vector content. Normalize svg+xml extensions to
.svg when generating media filenames so the relationship target matches
the stored media key.
…tadata

Only emit the empty-inline-SDT placeholder when the resolved metadata
describes a structuredContent node, so other inline SDT variants aren't
collapsed into a placeholder text run when their content is empty.
Parse data URIs by inspecting the meta header rather than assuming
base64 encoding. URL-encoded payloads (e.g. SVG with charset=utf-8)
are now decoded as text and written through as-is, while base64
payloads continue through atob/binary conversion. Adds coverage for
the non-base64 SVG path in handleBase64 and the browser
registration plugin.
Replace the base64-only data URL regex with an allowlist-based
validator that accepts URL-encoded SVG payloads while still
restricting raster image MIME types to base64. Applies to both
inline image runs and field annotation images, and adds tests for
the SVG, raster, and non-image cases.
luccas-harbour and others added 28 commits May 27, 2026 15:17
When backspace or delete targets an sdtContentLocked structured-content
SDT, collapse the selection to the wrapper boundary instead of letting
the keystroke fall through. Prevents the locked inline content from
being mutated while keeping the caret in a usable position for the next
edit.
Replace the invisible 8px spacer with a full "Click or tap here to enter text"
placeholder for both inline and block structured-content controls. The
placeholder is layout-only (no document text), styled via CSS pseudo-element,
and selected-node highlight inherits the system Highlight color.

Wire the new chrome into pointer mapping, caret geometry, and the input
manager so clicks on the placeholder land inside the SDT instead of snapping
to the wrapper boundary. ArrowLeft from the trailing boundary now re-enters
an empty inline SDT, and Backspace/Delete inside an unlocked inline SDT no
longer escapes into surrounding text.
Route empty block-SDT paragraphs through paragraphToFlowBlocks so the
placeholder run picks up the paragraph's resolved font, size, and color
instead of falling back to the document defaults. The painter now applies
those run styles to the placeholder span, keeping "Click or tap here to
enter text" visually consistent with the surrounding paragraph chrome.
Use `max-content` with `max-width: 130px` instead of stretching block SDT
labels to the chrome width, so short labels no longer span the entire
block. Inner span now flexes with `min-width: 0` to keep ellipsis behavior.
Stop maxing the measured placeholder width with the fallback — for empty
SDTs we now trust the measured value and only fall back when measurement
returns zero. Also treat empty-SDT placeholder runs as visible content so
chrome geometry (--sd-sdt-chrome-left/width) is emitted for them.
…int modes

Remove CSS rules that blanked the ::before content for empty SDT placeholders
in viewing and print modes so the placeholder prompt stays visible. Update
tests to assert the rules are absent.
…ments

fix(sdt): empty SDT placeholder text + cursor/keyboard interactions (SD-3237)
fix(super-editor): repair caret, keyboard, and drag interactions around block SDTs (SD-3237)
…ling-toolbar-when-selection-includes-a-locked-sdt

feat(super-editor): disable toolbar mutations inside content-locked SDTs (SD-3274)
@caio-pizzol caio-pizzol merged commit 4518925 into luccas/sd-3258-bug-image-resizing-inside-structured-content May 27, 2026
5 checks passed
@caio-pizzol caio-pizzol deleted the luccas/sd-3116-bug-image-set-as-presetcontent-in-sdt-field-does-not-persist branch May 27, 2026 23:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants