Skip to content

fix: support hyperlinks on DrawingML images (a:hlinkClick)#2552

Open
iguit0 wants to merge 2 commits intosuperdoc-dev:mainfrom
iguit0:fix/drawingml-image-hyperlinks
Open

fix: support hyperlinks on DrawingML images (a:hlinkClick)#2552
iguit0 wants to merge 2 commits intosuperdoc-dev:mainfrom
iguit0:fix/drawingml-image-hyperlinks

Conversation

@iguit0
Copy link
Copy Markdown

@iguit0 iguit0 commented Mar 25, 2026

Summary

DOCX images with a:hlinkClick hyperlinks rendered correctly in Word but clicking them in SuperDoc did nothing. The importer already parsed the relationship and stored attrs.hyperlink on the ProseMirror image node — but the hyperlink was silently dropped further down the pipeline, so the renderer never had a URL to render.

Root cause: a three-step gap between import and rendering:

  1. ImageBlock / ImageRun contract types had no hyperlink field
  2. imageNodeToBlock and imageNodeToRun did not forward the attr
  3. DomPainter.renderImageFragment / renderImageRun never created an anchor

What Changed

  • Added hyperlink?: { url: string; tooltip?: string } to both ImageBlock and ImageRun in the shared contracts package
  • Forwarded the attr in imageNodeToBlock and imageNodeToRun, with the same validation (non-empty string URL) used for other optional fields
  • Added DomPainter.buildImageHyperlinkAnchor() — a private helper that wraps an image element in <a class="superdoc-link"> when a valid hyperlink is present. Uses sanitizeHref (same guard as text links) and applies target/rel for external URLs with accessibility attributes mirroring applyLinkAttributes
  • Called the helper in renderImageFragment (block images) and at all three return points of renderImageRun (inline images with/without clip-path wrapper)

The existing EditorInputManager click-delegation already detects a.superdoc-link on pointer-down and dispatches superdoc-link-click. LinkClickHandler then opens the URL in viewing mode — no changes needed in either component. Unlinked images are unaffected: buildImageHyperlinkAnchor is a no-op when hyperlink is undefined, and image resize/selection behavior is preserved because the fragment <div> (which carries data-image-metadata) remains the outermost element.

Test Plan

  • Added 5 unit tests to pm-adapter/src/converters/image.test.ts — verifies hyperlink is forwarded with tooltip, without tooltip, and is omitted for null/empty cases
  • Added 4 integration tests to painters/dom/src/index.test.ts — verifies linked images render as <a class="superdoc-link"> with correct href, tooltip becomes title, unlinked images have no anchor, and unsafe URLs (e.g. javascript:) are blocked
  • All existing tests pass: pm-adapter (1,669), painter-dom (824), super-editor (9,697)
  • Manual: open a DOCX with a hyperlinked image → image renders → clicking in viewing mode opens the correct URL

Fixes #2065

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7bea148896

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".


const anchor = this.doc.createElement('a');
anchor.href = sanitized.href;
anchor.classList.add('superdoc-link');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve image selection for linked images in edit mode

Wrapping image elements with a.superdoc-link causes EditorInputManager.#handlePointerDown to take the link fast-path (closest('a.superdoc-link')) and return before the inline/block image selection paths run (#handleInlineImageClick / #handleFragmentClick). As a result, in editable documents, clicking a hyperlinked image no longer produces image node selection or resize-overlay activation, so linked images become effectively non-selectable by pointer.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@caio-pizzol caio-pizzol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@iguit0 nice contribution, thanks for tackling this! the approach looks good — checked the spec and the dual placement, relationship handling, and tooltip are all done right.

one small thing on tooltip encoding (left a suggestion). one optional cleanup on shared code.

tests: block-level is well covered. the inline paths (imageNodeToRun and the 3 renderer return paths) don't have hyperlink tests yet — would be great to add those.

left a few inline comments.

}
if (hyperlink.tooltip) {
anchor.title = hyperlink.tooltip;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

text links run the tooltip through encodeTooltip() which trims whitespace and caps it at 500 chars. this skips that — a crafted docx with a huge tooltip would render it in full. worth matching the text link behavior?

Suggested change
}
if (hyperlink.tooltip) {
const tooltipResult = encodeTooltip(hyperlink.tooltip);
if (tooltipResult?.text) {
anchor.title = tooltipResult.text;
}
}

...(flipH !== undefined && { flipH }),
...(flipV !== undefined && { flipV }),
// Image hyperlink from OOXML a:hlinkClick
...(() => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same parsing logic exists in inline-converters/image.ts:170. a small shared helper in utilities.ts would keep them in sync and also get rid of the IIFE here.

const fragment = {
kind: 'image' as const,
blockId: 'linked-img',
x: 20,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

layout is created here but never used — the tests build their own. can be removed.

@PeterHollens
Copy link
Copy Markdown

Nice work on this. I independently implemented the same fix on my fork while digging through #2065, and I converged on the same root cause.

One thing I found useful was reusing the existing shared link pipeline ( / / ) rather than introducing an image-specific hyperlink path, since it automatically inherits the current blocked-link handling, dataset propagation, tooltip accessibility, and other shared link behavior. I also added explicit inline-image coverage in both the PM adapter and DOM painter tests, since the inline paths were the main gap I noticed while reviewing.

If any of that is helpful, happy to share or compare against my branch here: https://github.com/PeterHollens/superdoc/tree/feature/drawingml-image-hyperlinks

@PeterHollens
Copy link
Copy Markdown

Nice work on this. I independently implemented the same fix on my fork while
digging through #2065, and I converged on the same root cause.

One thing I found useful was reusing the existing shared link pipeline
(FlowRunLink / buildFlowRunLink / wrapElementWithLink) rather than
introducing an image-specific hyperlink path, since it automatically inherits
the current blocked-link handling, dataset propagation, tooltip accessibility,
and other shared link behavior. I also added explicit inline-image coverage in
both the PM adapter and DOM painter tests, since the inline paths were the main
gap I noticed while reviewing.

If any of that is helpful, happy to share or compare against my branch here:
https://github.com/PeterHollens/superdoc/tree/feature/drawingml-image-hyperlinks

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.

Feature: Support hyperlink click on DrawingML images

3 participants