ElyMark: the page hands every link click to the app (M-05)

Preview clicks and Cmd/Ctrl clicks in the editor go through one
click handler; it stops the browser and sends the link mark's own
address, since Milkdown blanks unsafe hrefs in the DOM. Reviews run
at effort xhigh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-10 11:24:05 -04:00
1 parent fa5c0b7a75
commit c5f614d095
12 files changed
+160 -18

No files matched your search

+4 -1
View File
@@ -43,6 +43,9 @@ function flush() {
pending = null
}
/** A link the user opens; the app decides. */
const linked = (href) => tell({ kind: 'link', generation, href })
/** The shown editor's headings, for the app's outline. */
function outline() {
if (current) tell({ kind: 'outline', generation, items: current.outline() })
@@ -83,7 +86,7 @@ async function build(text, mode) {
const edited = (write) => changed(() => front + write())
let made
try {
made = await visual(place, body, mode === 'visual', texts, edited, edge)
made = await visual(place, body, mode === 'visual', texts, edited, edge, linked)
} catch (error) {
// A broken editor falls back to source and says so.
console.error(`ely: the visual editor failed: ${error.name}`)
+25 -1
View File
@@ -39,7 +39,7 @@ import { search } from 'prosemirror-search'
import { style } from './style.js'
/** An editor over `markdown`; null when blocks cannot map. */
export async function visual(root, markdown, editable, texts, changed, edge) {
export async function visual(root, markdown, editable, texts, changed, edge, linked) {
const owner = diagrams()
let write = () => ({ text: markdown, spans: [] })
// Setting up raw blocks is no edit.
@@ -71,6 +71,29 @@ export async function visual(root, markdown, editable, texts, changed, edge) {
},
}),
)
// Preview: a click opens. Editor: Cmd (Ctrl elsewhere).
const mac = navigator.platform.startsWith('Mac')
const links = $prose(
() =>
new Plugin({
props: {
handleDOMEvents: {
click: (view, event) => {
const anchor = event.target.closest?.('a')
if (!anchor || !view.dom.contains(anchor)) return false
event.preventDefault()
if (editable && !(mac ? event.metaKey : event.ctrlKey)) return true
// The DOM href is sanitized; the mark is not.
const node = view.state.doc.nodeAt(view.posAtDOM(anchor, 0))
const mark = node?.marks.find((mark) => mark.type.name === 'link')
if (!mark) throw new Error('ely: a link without its mark')
linked(mark.attrs.href)
return true
},
},
},
}),
)
// Past their history, undo and redo reach the page.
const ends = $prose(() =>
keymap({
@@ -103,6 +126,7 @@ export async function visual(root, markdown, editable, texts, changed, edge) {
.use(watch)
.use(current)
.use(ends)
.use(links)
.use($prose(() => search()))
.create()
const editor = await made.catch((error) => {