fix(live-preview): make image rendering AST-driven and linked images navigable#150
Open
rotiokivv wants to merge 8 commits into
Open
fix(live-preview): make image rendering AST-driven and linked images navigable#150rotiokivv wants to merge 8 commits into
rotiokivv wants to merge 8 commits into
Conversation
The live preview located images with a document-wide regex, blind to parse context. Three user-visible bugs follow from that root cause: - [](url) (e.g. README CI badges): the regex image range overlapped the link decoration, and the link marker arithmetic split at the first ](, garbling the label and the navigation URL. Links whose label contains an image now render as a clickable image widget. - Image syntax inside fenced code blocks / inline code was rendered as a real image instead of staying literal text. - URLs with balanced parentheses (.../Foo_(bar)) were truncated at the first ), breaking the image and leaving a stray paren. Images are now emitted by the mdast walk like every other inline node. The Lezer adapter preserves the image title so tooltip-dependent renderers keep working, and reference-style images without an inline URL stay raw text instead of becoming an empty-src widget. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Standalone images show source + a preview widget below when the cursor is inside their range. Linked images rendered raw source only, so pasting a badge line (cursor lands at its end, which counts as on the link) looked broken until the cursor moved away. Mirror the standalone image contract: cursor out - replace widget; cursor in - editable source plus a block preview widget right below. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CM6 skips every registered domEventHandler - including the [data-link-url] mousedown handler that opens links - for events inside widgets whose ignoreEvent() returns true, so the linked-image widget rendered but never navigated. Match the plain-link widget contract (ignoreEvent false); interactive chrome inside custom image renderers already protects itself by stopping propagation. Also surface the destination on hover: browsers resolve tooltips from the innermost titled element, so an image-level tooltip set by a renderer would mask the wrapper title. Inside a linked image the destination outranks it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The demo never installed a window-open handler, so every link click in the live preview (window.open from the renderer) spawned a bare child BrowserWindow instead of the user browser. Route http(s) targets through shell.openExternal and deny all other window opens; apply the same policy to will-navigate so stray <a href> navigations cannot replace the editor page (current-URL reloads stay allowed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two gaps in the linked-image branch, both found by adversarial review: - The image check only looked at the label top level, so [**](u) (image wrapped in emphasis) still fell into the marker-arithmetic path and rendered the garbage label the branch exists to prevent. Detect images at any depth and flatten wrapper nodes when rendering the label. - A Link node with no URL child (reference-style [x][ref], empty [x]()) fell back to the whole node source as its url, leaking raw markdown into data-link-url and the hover tooltip. Only autolinks keep the node-text fallback; such labels now render their image without pretending to navigate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
data-link-url is authored document content, so clicking a link whose destination is javascript:, file: or a custom scheme must not reach window.open. Scheme-less GFM autolink literals (www.example.com) used to be opened as relative paths and now get an https prefix instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…overage The click-to-navigate behavior hinges on the widget NOT swallowing events: CM6 skips every domEventHandler for events inside widgets whose ignoreEvent() returns true, and the rendered DOM is identical either way, so only a dispatched mousedown can catch that regression. Also cover: scheme filtering, single-preview suppression in edit mode, mixed text-and-images labels, all three CommonMark title delimiters, images inside raw HTML blocks, and scope the stray-paren assertion to the image line so unrelated renderer chrome cannot break it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The window-open / will-navigate rules were inline closures over mainWindow, so nothing could exercise them. Pull the pure decisions into link-policy.ts with unit tests, and let mailto: reach the system handler alongside http(s) — it was silently dead before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary / 摘要
Image ranges in the live preview now come from the AST instead of a document-wide regex scan, which fixes garbled/blank rendering of linked images (
[](url), the README-badge pattern), images being injected into code blocks, and truncated parenthesized URLs — and linked images are now clickable with the destination shown on hover.Motivation / 背景与动机
The live preview located images with a regex over the whole document (
collectImageRanges), blind to parse context. Three user-visible bugs shared that root cause:[](https://example.com)the link's marker arithmetic splits at the first](— inside the nested image — so the label rendered asgrew real image widgets — you couldn't write documentation about image syntax, and pasted code got corrupted visually.)was truncated at the first), leaving a broken image and a stray paren.Only the parser knows whether
sits in prose or inside a code span, and only the parser resolves balanced parens — so the fix is to emit image ranges from the AST walk like every other inline node and delete the regex scan.Changes / 变更内容
packages/core:live-preview-ranges.ts: deletecollectImageRanges; addimageto the AST-walk node set; skip reference-style images (no inline URL) so they stay raw text.live-preview.ts: new linked-image branch — links whose label contains an image (at any depth, incl. inside emphasis/strong/delete) render as a single clickable widget (data-link-url+ destination tooltip, including on inner<img>s, where it outranks renderer-set alt tooltips). Cursor-aware like standalone images: cursor out → replace widget; cursor in → editable source + block preview below (so a freshly pasted badge shows immediately). The widget must not swallow events (CM6 skips alldomEventHandlersforignoreEvent() === truewidgets, which would kill the navigation handler). Link navigation now filters schemes: onlyhttp(s)/mailto:reachwindow.open; scheme-less GFM autolinks (www.…) get anhttps://prefix instead of navigating relatively.lezer-mdast-adapter.ts: images keep their CommonMark title (— previously dropped, used by tooltip-rendering hosts); aLinknode with no inline URL (reference-style[x][ref], empty[x]()) now yieldsurl: ""instead of leaking the whole raw source as its URL (autolinks keep the node-text fallback).apps/electron-demo:electron/main.ts+ newelectron/link-policy.ts: the demo never installed a window-open handler, so link clicks spawned bare child BrowserWindows.http(s)/mailto:now route to the system browser viashell.openExternal; everything else is denied, andwill-navigateis guarded the same way (same-page reloads stay allowed). The policy is a pure, unit-tested module.Behavior notes
(empty alt) no longer shows the URL as a text label next to the image — the adapter emitsalt: ""(spec-consistent with remark) where the old regex emittednulland the default renderer fell back to the URL.mailto:links in the demo used to open a broken child window; they now open the system mail handler.ce3e5d2bundles the navigation fix with the hover-tooltip change; they land together because both exist to make the linked-image widget behave like a link.Testing / 测试
pnpm testpasses / 全绿 — 533 tests across 34 filespnpm build) / 受影响包构建通过(含pnpm typecheck)live-preview-ranges.test.ts: no image ranges in fenced/inline code or raw HTML blocks; balanced-paren URLs; all three CommonMark title delimiter forms (+ no-title →null); linked-image emits link + suppressed child ranges; reference-style images/links stay raw / geturl: "".live-preview-regressions.test.ts: linked image renders as clickable widget (attrs, tooltip on wrapper and<img>); cursor-in shows raw source + exactly one preview (parentSpans suppression); a dispatchedmousedowncallswindow.open(url, "_blank", "noopener")— this is the only way to catch a regression to an event-swallowing widget, since the DOM is identical either way; scheme filtering (javascript:consumed,www.…prefixed); labels with images nested in emphasis; mixed text-and-multiple-images labels; reference-style linked image renders without a fake destination.apps/electron-demo/test/link-policy.test.ts: external-target allowlist and same-page-reload rules.```blocks and inline code showas literal text;**bold**and images now behave consistently in code contexts.Compliance / 合规自检
AI-assisted notes / AI 使用说明:
AI assistance (Claude Code) was used for implementation and test drafting under my direction: I selected the bugs, reproduced them, drove the design decisions (AST-driven ranges over regex, matching the standalone-image cursor contract, event-swallowing tradeoffs), verified every scenario manually in the demo, and reviewed the final diff line by line. I can walk through and defend any part of the change.
Checklist / 自检清单
live-preview-table.ts→ n/a, not touchedScreenshots / Recordings · 截图或录屏 (UI changes)
Same document, electron-demo. Test document:
Before (main): the linked image renders as the garbled label
![CI badge; image widgets are injected inside the fenced block and inline code; the parenthesized URL is truncated (404 badge not found+ stray-works-blue)text).After (this branch): the badge renders as a clickable image (hover shows the destination, click opens the system browser); code contexts stay literal; the parenthesized URL loads in full.
After, cursor on the badge line: the raw source stays editable with the live preview rendered right below — same contract as standalone images.