Skip to content

Let an embedder route Markdown URL clicks through their own model - #166

Closed
paulscan wants to merge 1 commit into
nodes-app:mainfrom
paulscan:feat/url-link-click-callback
Closed

paulscan wants to merge 1 commit into
nodes-app:mainfrom
paulscan:feat/url-link-click-callback

Conversation

@paulscan

@paulscan paulscan commented Aug 31, 2026 •

Copy link
Copy Markdown

NativeTextViewWrapper.onLinkClick already covers [[Name]] clicks. A click on a regular [label](href) link falls through to AppKit's NSWorkspace.open with no seam for the embedder to intercept.

An embedder whose Markdown URLs mean something outside of the web has no way to route the click. Relative paths against a base directory, opaque ids, and custom schemes all require intercepting textView(_:clickedOnLink:at:) from outside the engine. The current workaround is to walk the view tree, find the underlying NSTextView, and install a proxy delegate that steals the method.

What's added

NativeTextViewWrapper.onURLLinkClick: ((URL, NSRange) -> Bool)? closes the gap. The closure receives the URL that the styler wrote onto the label's .link attribute and the label's range in the current display text. The return value decides who handles the click. Returning true consumes it and skips AppKit's default handler. Returning false (or leaving the hook nil) falls back to the previous behavior. The closure runs synchronously because the delegate's return value has to travel back into AppKit on the same call.

updateNSView refreshes the hook alongside the other coordinator callbacks, so an embedder can swap it between updates without remounting.

Wiki links still route through onLinkClick. The URL hook only fires on [label](href).

Tests

Tests/MarkdownEngineTests/URLLinkClickCallbackTests.swift covers four behaviors:

  • Returning true consumes the click and reports the URL plus the
    label range.
  • Returning false hands the click back to AppKit.
  • An unset hook preserves the previous behavior.
  • A wiki-link click bypasses the URL hook and still routes through
    onLinkClick.

swift build and swift test are green.

`onLinkClick` already covers `[[Name]]` clicks, but a click on a regular
`[label](href)` fell straight through to AppKit's `NSWorkspace.open`. An
embedder whose Markdown URLs are meaningful outside of the web — a
relative path resolved against a base directory, an opaque id, a custom
scheme registered elsewhere in the app — had no seam here: intercepting
the click meant reaching into the underlying `NSTextView` from outside
and installing a delegate proxy that stole `textView(_:clickedOnLink:at:)`.

`NativeTextViewWrapper.onURLLinkClick` closes that gap. The closure
receives the URL the styler wrote onto the label's `.link` attribute
(so `LinkStyle.bareHrefs` decides whether a bare href arrived
`https://`-prepended or as authored) and the label's range in the
current display text. Its return value decides who handles the click:
`true` consumes it and skips AppKit's default handler, `false` (or
leaving the hook nil) falls back to the previous behavior. It runs
synchronously because the delegate's return value has to travel back
into AppKit on the same call.

Wiki-link clicks still go through `onLinkClick` — the URL hook is
`[label](href)`-only, and a `[[Name]]` click reaches its embedder
handler exactly as before.

`Tests/MarkdownEngineTests/URLLinkClickCallbackTests.swift` pins the
four behaviors: returning true consumes and reports the URL + label
range, returning false falls through, an unset hook preserves the
historical behavior, and a wiki-link click bypasses the URL hook and
still routes through `onLinkClick`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LNvrf6TPFQ571njp1G98wt
@paulscan
paulscan marked this pull request as ready for review August 31, 2026 00:39
@yvos

yvos commented Sep 27, 2026

Copy link
Copy Markdown
Contributor

Hi @paulscan, I ran into the same problem from a different angle: relative .md paths that my app resolves to its own records. I've opened #190 proposing a single "host decides" activation seam that covers this PR and #165 together. It passes the destination as written, the source range and the modifiers, and lets the host return whether it handled the click. Would that shape work for your case? If so, I'd be glad to combine efforts and credit you in the PR.

@paulscan

Copy link
Copy Markdown
Author

Hi @paulscan, I ran into the same problem from a different angle: relative .md paths that my app resolves to its own records. I've opened #190 proposing a single "host decides" activation seam that covers this PR and #165 together. It passes the destination as written, the source range and the modifiers, and lets the host return whether it handled the click. Would that shape work for your case? If so, I'd be glad to combine efforts and credit you in the PR.

@yvos - yep, your approach is better than mine. I'll close #165 and #166 once your PR is open. Passing the destination as written removes the need for #165, and it fixes angle-bracket and titled destinations, which reach #166's hook mangled today. Running the edit zone before the callback is good.

One change I'd ask for: markdownLinkDestination stops at the first whitespace, so [x](my file.md) yields my and [x](../Foo Bar/) yields ../Foo. The engine still renders both as links, and they route with the full text today. Please strip a title only when what follows the whitespace is a quoted or parenthesized title, and otherwise return the whole trimmed destination, with a test for that case.

Minor, take it or leave it: the autolink path runs NSDataDetector over the whole document on each click, and limiting it to the clicked paragraph would be more efficient.

@paulscan

Copy link
Copy Markdown
Author

Superseded by #192

@yvos

yvos commented Sep 27, 2026

Copy link
Copy Markdown
Contributor

Thanks @paulscan! Both points are in #192 now: destinations keep interior spaces and only a quoted or parenthesized title is stripped (with tests for my file.md and ../Foo Bar/), and the autolink detector only scans the clicked paragraph. I've credited you in the PR and marked it ready for review.

paulscan added a commit to paulscan/swift-markdown-engine that referenced this pull request Sep 27, 2026
nodes-app#192 replaces nodes-app#166 with one host-first
seam for inline links, wiki links and autolinks. It passes the
destination as written instead of a URL, so angle-bracket and titled
destinations and hrefs URL(string:) rejects route correctly.

This reverts commit 737b9c7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sc1cCgT31d7YCSkPURksAo
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants