@inkandswitch/patchwork-pkg-base 0.0.120 → 0.0.122

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- automerge:3S9mtBoc4MX2vze41wj3ULwuSUQv
1
+ automerge:BQ45jRvfLwfaVSXvcbUquxQViiA
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inkandswitch/patchwork-pkg-base",
3
- "version": "0.0.120",
3
+ "version": "0.0.122",
4
4
  "type": "module",
5
5
  "description": "Base packages for the Patchwork system",
6
6
  "repository": {
@@ -111,49 +111,91 @@ function isImageDoc(doc) {
111
111
  if (!(doc.content instanceof Uint8Array)) return false;
112
112
  return !!doc.mimeType?.startsWith("image/") || IMAGE_EXTENSIONS.includes(doc.extension?.toLowerCase() ?? "");
113
113
  }
114
+ var seen = /* @__PURE__ */ new Map();
115
+ function imageFor(docId, doc) {
116
+ const prev = seen.get(docId);
117
+ if (prev?.kind === "image" && prev.content === doc.content) return prev;
118
+ if (prev?.kind === "image") URL.revokeObjectURL(prev.url);
119
+ const content = doc.content;
120
+ const next = {
121
+ kind: "image",
122
+ content,
123
+ url: URL.createObjectURL(
124
+ new Blob([content], { type: doc.mimeType || "" })
125
+ ),
126
+ name: doc.name ?? ""
127
+ };
128
+ seen.set(docId, next);
129
+ return next;
130
+ }
114
131
  function mountImageOrFrame(docId, container, mountFrame) {
115
- const repo = window.repo;
116
- if (!repo?.find) {
132
+ const known = seen.get(docId);
133
+ if (known?.kind === "frame") {
117
134
  mountFrame();
118
135
  return () => {
119
136
  };
120
137
  }
138
+ const repo = window.repo;
121
139
  let disposed = false;
122
- let objectUrl;
123
140
  let handle;
124
141
  let img;
142
+ const show = (image) => {
143
+ if (!img) {
144
+ container.classList.add("cm-embed-image");
145
+ img = document.createElement("img");
146
+ img.addEventListener("load", () => {
147
+ const current = seen.get(docId);
148
+ if (current?.kind !== "image" || img.src !== current.url) return;
149
+ current.width = img.naturalWidth;
150
+ current.height = img.naturalHeight;
151
+ img.width = current.width;
152
+ img.height = current.height;
153
+ });
154
+ container.appendChild(img);
155
+ }
156
+ if (img.getAttribute("src") === image.url) return;
157
+ if (image.width && image.height) {
158
+ img.width = image.width;
159
+ img.height = image.height;
160
+ } else {
161
+ img.removeAttribute("width");
162
+ img.removeAttribute("height");
163
+ }
164
+ img.alt = image.name;
165
+ img.src = image.url;
166
+ };
125
167
  const render = () => {
126
168
  const doc = handle.doc();
127
- if (!isImageDoc(doc)) return;
128
- if (objectUrl) URL.revokeObjectURL(objectUrl);
129
- objectUrl = URL.createObjectURL(
130
- new Blob([doc.content], { type: doc.mimeType || "" })
131
- );
132
- img.src = objectUrl;
133
- img.alt = doc.name ?? "";
169
+ if (isImageDoc(doc)) show(imageFor(docId, doc));
134
170
  };
171
+ if (known?.kind === "image") show(known);
172
+ if (!repo?.find) {
173
+ if (!img) mountFrame();
174
+ return () => {
175
+ };
176
+ }
135
177
  Promise.resolve(repo.find(`automerge:${docId}`)).then(
136
178
  (h) => {
137
179
  if (disposed) return;
138
180
  handle = h;
139
- if (!isImageDoc(handle.doc())) {
181
+ const doc = handle.doc();
182
+ if (!isImageDoc(doc)) {
183
+ if (doc) seen.set(docId, { kind: "frame" });
184
+ img?.remove();
185
+ container.classList.remove("cm-embed-image");
140
186
  mountFrame();
141
187
  return;
142
188
  }
143
- container.classList.add("cm-embed-image");
144
- img = document.createElement("img");
145
- container.appendChild(img);
146
189
  render();
147
190
  handle.on("change", render);
148
191
  },
149
192
  () => {
150
- if (!disposed) mountFrame();
193
+ if (!disposed && !img) mountFrame();
151
194
  }
152
195
  );
153
196
  return () => {
154
197
  disposed = true;
155
198
  handle?.off?.("change", render);
156
- if (objectUrl) URL.revokeObjectURL(objectUrl);
157
199
  };
158
200
  }
159
201
  var EMBED_PATTERN = /\[patchwork:([^/\]]+)(?:\/([^\]]+))?\]/g;
@@ -190,10 +232,10 @@ function urlToDocId(raw) {
190
232
  }
191
233
  function extractDocRefs(dt) {
192
234
  const refs = [];
193
- const seen = /* @__PURE__ */ new Set();
235
+ const seen2 = /* @__PURE__ */ new Set();
194
236
  const push = (docId, toolId) => {
195
- if (!docId || seen.has(docId)) return;
196
- seen.add(docId);
237
+ if (!docId || seen2.has(docId)) return;
238
+ seen2.add(docId);
197
239
  refs.push({ docId, toolId });
198
240
  };
199
241
  const dnd = dt.getData(PATCHWORK_DND);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/extensions/embed.ts", "../../src/themes/embed.ts"],
4
- "sourcesContent": ["import {\n EditorView,\n Decoration,\n WidgetType,\n ViewPlugin,\n ViewUpdate,\n dropCursor,\n type DecorationSet,\n} from \"@codemirror/view\";\nimport { Range } from \"@codemirror/state\";\nimport {\n type AutomergeUrl,\n type DocumentId,\n isValidDocumentId,\n parseAutomergeUrl,\n} from \"@automerge/automerge-repo/slim\";\nimport { embedTheme } from \"../themes/embed.ts\";\n\n/**\n * Renders an embedded doc through the shared \"embed\" tool, which draws the\n * chrome and nests the content view. The inner tool travels via the\n * `embed-tool-id` attribute; tool picks come back as\n * `patchwork:embed-tool-changed` events and are persisted by rewriting the\n * marker (which recreates the widget).\n */\nclass EmbedWidget extends WidgetType {\n readonly docId: DocumentId;\n // `null` = no pinned tool; the embed frame uses the datatype's fallback.\n readonly toolId: string | null;\n readonly embedText: string;\n\n constructor(docId: DocumentId, toolId: string | null, embedText: string) {\n super();\n this.docId = docId;\n this.toolId = toolId;\n this.embedText = embedText;\n }\n\n eq(other: EmbedWidget) {\n return other.docId === this.docId && other.toolId === this.toolId;\n }\n\n toDOM(view: EditorView) {\n const container = document.createElement(\"div\");\n container.className = \"cm-embed\";\n\n // An image with no pinned tool is just drawn, with no frame round it. We\n // can't tell until the doc loads, so hold off mounting the frame until then.\n if (this.toolId) {\n this.mountFrame(view, container);\n } else {\n const cleanup = mountImageOrFrame(this.docId, container, () =>\n this.mountFrame(view, container)\n );\n cleanups.set(container, cleanup);\n }\n return container;\n }\n\n destroy(dom: HTMLElement) {\n cleanups.get(dom)?.();\n cleanups.delete(dom);\n }\n\n private mountFrame(view: EditorView, container: HTMLElement): void {\n const patchworkView = document.createElement(\"patchwork-view\");\n // No heads in the url: the embed follows whatever version the host\n // resolves \u2014 the active checkpoint when one is checked out, live otherwise.\n patchworkView.setAttribute(\"doc-url\", `automerge:${this.docId}`);\n patchworkView.setAttribute(\"tool-id\", \"embed\");\n if (this.toolId) patchworkView.setAttribute(\"embed-tool-id\", this.toolId);\n // Explicit inline height: without it the element collapses to 0px (the\n // stylesheet rule isn't reliably applied here).\n patchworkView.style.display = \"block\";\n patchworkView.style.height = \"500px\";\n patchworkView.style.width = \"100%\";\n\n // Persist tool picks into the marker text. Stop propagation so nested\n // embeds don't also change the outer embed's tool.\n container.addEventListener(\"patchwork:embed-tool-changed\", (e) => {\n e.stopPropagation();\n const toolId = (e as CustomEvent<{ toolId?: string }>).detail?.toolId;\n if (toolId) this.setTool(view, container, toolId);\n });\n\n container.appendChild(patchworkView);\n }\n\n private setTool(\n view: EditorView,\n container: HTMLElement,\n toolId: string\n ): void {\n const from = view.posAtDOM(container);\n const to = from + this.embedText.length;\n // Only rewrite if the marker is still exactly where this widget maps to.\n if (view.state.doc.sliceString(from, to) !== this.embedText) return;\n view.dispatch({\n changes: { from, to, insert: embedSyntax({ docId: this.docId, toolId }) },\n });\n }\n\n ignoreEvent() {\n // Atomic embed: the editor ignores all events; the frame handles them.\n return true;\n }\n}\n\nconst cleanups = new WeakMap<HTMLElement, () => void>();\n\nconst IMAGE_EXTENSIONS = [\"svg\", \"png\", \"jpg\", \"jpeg\", \"gif\", \"webp\", \"bmp\"];\n\ntype FileDoc = {\n \"@patchwork\"?: { type?: string };\n content?: unknown;\n mimeType?: string;\n extension?: string;\n name?: string;\n};\n\nfunction isImageDoc(doc: FileDoc | undefined): boolean {\n if (doc?.[\"@patchwork\"]?.type !== \"file\") return false;\n if (!(doc.content instanceof Uint8Array)) return false;\n return (\n !!doc.mimeType?.startsWith(\"image/\") ||\n IMAGE_EXTENSIONS.includes(doc.extension?.toLowerCase() ?? \"\")\n );\n}\n\n/**\n * Look the doc up; if it's an image file, draw it as a bare `<img>` that\n * follows the doc's content. Anything else (or no `window.repo`) gets the\n * normal embed frame. Returns a cleanup for when the widget goes away.\n *\n * Reads through the realm-local `window.repo`, so an image changed only in a\n * draft shows its main-branch bytes here.\n */\nfunction mountImageOrFrame(\n docId: DocumentId,\n container: HTMLElement,\n mountFrame: () => void\n): () => void {\n const repo = (window as unknown as { repo?: any }).repo;\n if (!repo?.find) {\n mountFrame();\n return () => {};\n }\n\n let disposed = false;\n let objectUrl: string | undefined;\n let handle: any;\n let img: HTMLImageElement | undefined;\n\n const render = () => {\n const doc = handle.doc() as FileDoc | undefined;\n if (!isImageDoc(doc)) return;\n if (objectUrl) URL.revokeObjectURL(objectUrl);\n objectUrl = URL.createObjectURL(\n new Blob([doc!.content as BlobPart], { type: doc!.mimeType || \"\" })\n );\n img!.src = objectUrl;\n img!.alt = doc!.name ?? \"\";\n };\n\n Promise.resolve(repo.find(`automerge:${docId}`)).then(\n (h) => {\n if (disposed) return;\n handle = h;\n if (!isImageDoc(handle.doc())) {\n mountFrame();\n return;\n }\n container.classList.add(\"cm-embed-image\");\n img = document.createElement(\"img\");\n container.appendChild(img);\n render();\n handle.on(\"change\", render);\n },\n () => {\n if (!disposed) mountFrame();\n }\n );\n\n return () => {\n disposed = true;\n handle?.off?.(\"change\", render);\n if (objectUrl) URL.revokeObjectURL(objectUrl);\n };\n}\n\n// Marker syntax: [patchwork:docId] or [patchwork:docId/toolId] (ids can't\n// contain `/` or `]`). We scan plain text instead of the markdown syntax tree\n// on purpose: `@codemirror/language` isn't a shared singleton across\n// patchwork's separately-bundled extensions, so `syntaxTree(state)` here would\n// read a different `Language` facet and always come back empty.\nconst EMBED_PATTERN = /\\[patchwork:([^/\\]]+)(?:\\/([^\\]]+))?\\]/g;\n\nfunction getEmbedLinks(view: EditorView) {\n const widgets: Range<Decoration>[] = [];\n const { state } = view;\n\n // Visible ranges are line-aligned and markers never span lines, so a marker\n // is always fully inside or fully outside a range.\n for (const { from, to } of view.visibleRanges) {\n const text = state.doc.sliceString(from, to);\n EMBED_PATTERN.lastIndex = 0;\n let m: RegExpExecArray | null;\n while ((m = EMBED_PATTERN.exec(text)) !== null) {\n const matchFrom = from + m.index;\n const matchTo = matchFrom + m[0].length;\n const [matchText, docId, toolId] = m;\n\n if (!isValidDocumentId(docId)) continue;\n\n const embed = Decoration.replace({\n widget: new EmbedWidget(docId as DocumentId, toolId ?? null, matchText),\n });\n widgets.push(embed.range(matchFrom, matchTo));\n }\n }\n\n // `true` lets CodeMirror sort the ranges defensively (matches are already in\n // document order, but this is cheap insurance).\n return Decoration.set(widgets, true);\n}\n\n// MIME types we accept document drags from. Mirrors the sideboard's convention.\nconst PATCHWORK_DND = \"text/x-patchwork-dnd\";\nconst PATCHWORK_URLS = \"text/x-patchwork-urls\";\n\ntype DocRef = { docId: DocumentId; toolId: string | null };\n\n/** Turn an automerge url or a patchwork web link into a DocumentId, or null. */\nfunction urlToDocId(raw: string): DocumentId | null {\n const trimmed = raw.trim();\n if (!trimmed) return null;\n // Plain automerge url (optionally carrying heads/query/subpath).\n const am = trimmed.match(/automerge:([a-zA-Z0-9]+)/);\n if (am && isValidDocumentId(am[1])) return am[1] as DocumentId;\n // Patchwork web link: #doc=<documentId> (also &doc= / ?doc=).\n const web = trimmed.match(/[#&?]doc=([a-zA-Z0-9]+)/);\n if (web && isValidDocumentId(web[1])) return web[1] as DocumentId;\n return null;\n}\n\n/**\n * Read dragged documents out of a drop event, in order of format preference.\n * Only an *explicit* `toolId` on a structured item pins the tool; otherwise we\n * embed tool-less and let the view fall back to the datatype's default tool.\n */\nfunction extractDocRefs(dt: DataTransfer): DocRef[] {\n const refs: DocRef[] = [];\n const seen = new Set<string>();\n const push = (docId: DocumentId | null, toolId: string | null) => {\n if (!docId || seen.has(docId)) return;\n seen.add(docId);\n refs.push({ docId, toolId });\n };\n\n const dnd = dt.getData(PATCHWORK_DND);\n if (dnd) {\n try {\n const parsed = JSON.parse(dnd) as {\n items?: Array<{ url?: string; toolId?: string }>;\n };\n for (const item of parsed?.items ?? []) {\n if (item?.url) push(urlToDocId(item.url), item.toolId ?? null);\n }\n } catch {\n // fall through to the other formats\n }\n }\n if (refs.length > 0) return refs;\n\n const urls = dt.getData(PATCHWORK_URLS);\n if (urls) {\n try {\n const parsed: unknown = JSON.parse(urls);\n if (Array.isArray(parsed)) {\n for (const u of parsed) push(urlToDocId(String(u)), null);\n }\n } catch {\n // fall through\n }\n }\n if (refs.length > 0) return refs;\n\n const text = dt.getData(\"text/uri-list\") || dt.getData(\"text/plain\");\n if (text) {\n for (const line of text.split(/\\r?\\n/)) {\n if (line.startsWith(\"#\")) continue; // uri-list comments\n push(urlToDocId(line), null);\n }\n }\n return refs;\n}\n\nfunction embedSyntax({ docId, toolId }: DocRef): string {\n return toolId ? `[patchwork:${docId}/${toolId}]` : `[patchwork:${docId}]`;\n}\n\n/**\n * Import each OS file (dropped from the desktop or pasted) as a Patchwork\n * `file` document, returning tool-less refs to embed. Uses the realm-local\n * `window.repo` (the documented global) \u2014 fine for creating brand-new docs,\n * which aren't subject to draft remapping.\n */\nasync function importFileRefs(files: Iterable<File>): Promise<DocRef[]> {\n const repo = (window as unknown as { repo?: any }).repo;\n if (!repo) {\n console.warn(\"[codemirror-embed] window.repo unavailable; ignoring files\");\n return [];\n }\n const refs: DocRef[] = [];\n for (const file of files) {\n try {\n const mimeType = file.type || \"application/octet-stream\";\n const isText =\n mimeType.startsWith(\"text/\") || mimeType === \"application/json\";\n const content = isText\n ? await file.text()\n : new Uint8Array(await file.arrayBuffer());\n const parts = file.name.split(\".\");\n const extension = parts.length > 1 ? parts.pop()! : \"\";\n const handle = repo.create();\n handle.change((d: any) => {\n d[\"@patchwork\"] = { type: \"file\" };\n d.content = content;\n d.mimeType = mimeType;\n d.extension = extension;\n d.name = file.name;\n });\n const { documentId } = parseAutomergeUrl(handle.url as AutomergeUrl);\n if (isValidDocumentId(documentId)) {\n refs.push({ docId: documentId as DocumentId, toolId: null });\n }\n } catch (err) {\n console.warn(\n \"[codemirror-embed] failed to import file\",\n file.name,\n err\n );\n }\n }\n return refs;\n}\n\n/** Insert embeds over `from..to` (a drop point, or a pasted-over selection). */\nfunction insertRefs(\n view: EditorView,\n from: number,\n refs: DocRef[],\n to = from\n): void {\n if (refs.length === 0) return;\n // Files import asynchronously, so the doc may have shrunk in the meantime.\n const len = view.state.doc.length;\n from = Math.min(from, len);\n to = Math.min(to, len);\n let text = refs.map(embedSyntax).join(\"\\n\\n\");\n // On a non-empty line, add just enough breaks to put the embed on its own line.\n const { doc } = view.state;\n const start = doc.lineAt(from);\n const end = doc.lineAt(to);\n if (/\\S/.test(start.text) && from > start.from) text = \"\\n\" + text;\n if (/\\S/.test(end.text) && to < end.to) text = text + \"\\n\";\n view.dispatch({\n changes: { from, to, insert: text },\n selection: { anchor: from + text.length },\n });\n}\n\n/**\n * Drop handler that accepts:\n * - documents dragged from the sidebar / canvases / other tools\n * (`text/x-patchwork-dnd`, `text/x-patchwork-urls`, `text/uri-list`, and\n * `text/plain` patchwork links), and\n * - files dragged in from the operating system (imported as `file` docs).\n * Inserts `[patchwork:docId]` (or `[patchwork:docId/toolId]`) at the drop point.\n */\nfunction embedDropHandlers() {\n // Only claim the *dragover* for unambiguous patchwork/OS drags \u2014 plain\n // text/uri-list can be an ordinary in-editor text drag, which we must not\n // swallow. (`Files` covers OS drags, whose `dt.files` is empty until drop.)\n const wantsDragover = (dt: DataTransfer | null): boolean =>\n !!dt &&\n (dt.types.includes(\"Files\") ||\n dt.types.includes(PATCHWORK_DND) ||\n dt.types.includes(PATCHWORK_URLS));\n\n return EditorView.domEventHandlers({\n dragover(event) {\n const dt = event.dataTransfer;\n if (!wantsDragover(dt)) return false;\n event.preventDefault();\n // Doc drags insert a *reference* to the same automerge doc, so show the\n // link cursor; OS file drags genuinely copy content into new docs.\n if (dt) dt.dropEffect = dt.types.includes(\"Files\") ? \"copy\" : \"link\";\n return true;\n },\n drop(event, view) {\n const dt = event.dataTransfer;\n if (!dt) return false;\n const pos = view.posAtCoords({ x: event.clientX, y: event.clientY });\n if (pos == null) return false;\n\n // OS files import asynchronously; insert once the docs exist.\n if (dt.files && dt.files.length > 0) {\n event.preventDefault();\n void importFileRefs(Array.from(dt.files)).then((refs) =>\n insertRefs(view, pos, refs)\n );\n return true;\n }\n\n // Otherwise only handle the drop if it actually resolves to patchwork\n // docs (dnd/urls always do; uri-list/plain only for patchwork links).\n // If not, let CodeMirror handle it as a normal text drop.\n const refs = extractDocRefs(dt);\n if (refs.length === 0) return false;\n event.preventDefault();\n insertRefs(view, pos, refs);\n return true;\n },\n paste(event, view) {\n // Image pastes (screenshots, \"Copy Image\") become embedded `file` docs.\n // A paste that also carries text is left to CodeMirror: rich text from\n // office apps attaches a picture of itself, and a Finder copy carries\n // the file's icon alongside its name.\n const dt = event.clipboardData;\n if (!dt || dt.getData(\"text/plain\")) return false;\n const images = Array.from(dt.files ?? []).filter((f) =>\n f.type.startsWith(\"image/\")\n );\n if (images.length === 0) return false;\n event.preventDefault();\n const { from, to } = view.state.selection.main;\n void importFileRefs(images).then((refs) =>\n insertRefs(view, from, refs, to)\n );\n return true;\n },\n });\n}\n\nconst embedPlugin = ViewPlugin.fromClass(\n class {\n decorations: DecorationSet;\n\n constructor(view: EditorView) {\n this.decorations = getEmbedLinks(view);\n }\n\n update(update: ViewUpdate) {\n // Viewport too, so newly-visible markers get decorated.\n if (update.docChanged || update.viewportChanged) {\n this.decorations = getEmbedLinks(update.view);\n }\n }\n },\n {\n decorations: (v) => v.decorations,\n // Atomic: the cursor skips embeds and delete removes the whole marker.\n provide: (plugin) =>\n EditorView.atomicRanges.of(\n (view) => view.plugin(plugin)?.decorations ?? Decoration.none\n ),\n }\n);\n\nexport function markdownEmbed() {\n // `dropCursor` observes events, so it coexists with our dragover handler.\n return [embedPlugin, embedTheme, embedDropHandlers(), dropCursor()];\n}\n", "import { EditorView } from \"@codemirror/view\";\n\n// Outer container only; all embed chrome lives in the shared \"embed\" tool.\nexport const embedTheme = EditorView.baseTheme({\n \".cm-embed\": {\n display: \"block\",\n border: \"var(--studio-border-width, 1px) var(--studio-border-style, solid)\",\n borderRadius: \"4px\",\n overflow: \"hidden\",\n },\n \"&light .cm-embed\": {\n borderColor: \"#ddd\",\n },\n \"&dark .cm-embed\": {\n borderColor: \"#333\",\n },\n // Images are just images: no border, natural size, never wider than the text.\n \"&light .cm-embed.cm-embed-image, &dark .cm-embed.cm-embed-image\": {\n border: \"none\",\n borderRadius: \"0\",\n },\n \".cm-embed-image > img\": {\n display: \"block\",\n maxWidth: \"100%\",\n height: \"auto\",\n },\n \".cm-embed > patchwork-view\": {\n display: \"block\",\n height: \"500px\",\n width: \"100%\",\n },\n});\n"],
5
- "mappings": "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",
6
- "names": ["EditorView", "EditorView", "refs"]
4
+ "sourcesContent": ["import {\n EditorView,\n Decoration,\n WidgetType,\n ViewPlugin,\n ViewUpdate,\n dropCursor,\n type DecorationSet,\n} from \"@codemirror/view\";\nimport { Range } from \"@codemirror/state\";\nimport {\n type AutomergeUrl,\n type DocumentId,\n isValidDocumentId,\n parseAutomergeUrl,\n} from \"@automerge/automerge-repo/slim\";\nimport { embedTheme } from \"../themes/embed.ts\";\n\n/**\n * Renders an embedded doc through the shared \"embed\" tool, which draws the\n * chrome and nests the content view. The inner tool travels via the\n * `embed-tool-id` attribute; tool picks come back as\n * `patchwork:embed-tool-changed` events and are persisted by rewriting the\n * marker (which recreates the widget).\n */\nclass EmbedWidget extends WidgetType {\n readonly docId: DocumentId;\n // `null` = no pinned tool; the embed frame uses the datatype's fallback.\n readonly toolId: string | null;\n readonly embedText: string;\n\n constructor(docId: DocumentId, toolId: string | null, embedText: string) {\n super();\n this.docId = docId;\n this.toolId = toolId;\n this.embedText = embedText;\n }\n\n eq(other: EmbedWidget) {\n return other.docId === this.docId && other.toolId === this.toolId;\n }\n\n toDOM(view: EditorView) {\n const container = document.createElement(\"div\");\n container.className = \"cm-embed\";\n\n // An image with no pinned tool is just drawn, with no frame round it. We\n // can't tell until the doc loads, so hold off mounting the frame until then.\n if (this.toolId) {\n this.mountFrame(view, container);\n } else {\n const cleanup = mountImageOrFrame(this.docId, container, () =>\n this.mountFrame(view, container)\n );\n cleanups.set(container, cleanup);\n }\n return container;\n }\n\n destroy(dom: HTMLElement) {\n cleanups.get(dom)?.();\n cleanups.delete(dom);\n }\n\n private mountFrame(view: EditorView, container: HTMLElement): void {\n const patchworkView = document.createElement(\"patchwork-view\");\n // No heads in the url: the embed follows whatever version the host\n // resolves \u2014 the active checkpoint when one is checked out, live otherwise.\n patchworkView.setAttribute(\"doc-url\", `automerge:${this.docId}`);\n patchworkView.setAttribute(\"tool-id\", \"embed\");\n if (this.toolId) patchworkView.setAttribute(\"embed-tool-id\", this.toolId);\n // Explicit inline height: without it the element collapses to 0px (the\n // stylesheet rule isn't reliably applied here).\n patchworkView.style.display = \"block\";\n patchworkView.style.height = \"500px\";\n patchworkView.style.width = \"100%\";\n\n // Persist tool picks into the marker text. Stop propagation so nested\n // embeds don't also change the outer embed's tool.\n container.addEventListener(\"patchwork:embed-tool-changed\", (e) => {\n e.stopPropagation();\n const toolId = (e as CustomEvent<{ toolId?: string }>).detail?.toolId;\n if (toolId) this.setTool(view, container, toolId);\n });\n\n container.appendChild(patchworkView);\n }\n\n private setTool(\n view: EditorView,\n container: HTMLElement,\n toolId: string\n ): void {\n const from = view.posAtDOM(container);\n const to = from + this.embedText.length;\n // Only rewrite if the marker is still exactly where this widget maps to.\n if (view.state.doc.sliceString(from, to) !== this.embedText) return;\n view.dispatch({\n changes: { from, to, insert: embedSyntax({ docId: this.docId, toolId }) },\n });\n }\n\n ignoreEvent() {\n // Atomic embed: the editor ignores all events; the frame handles them.\n return true;\n }\n}\n\nconst cleanups = new WeakMap<HTMLElement, () => void>();\n\nconst IMAGE_EXTENSIONS = [\"svg\", \"png\", \"jpg\", \"jpeg\", \"gif\", \"webp\", \"bmp\"];\n\ntype FileDoc = {\n \"@patchwork\"?: { type?: string };\n content?: unknown;\n mimeType?: string;\n extension?: string;\n name?: string;\n};\n\nfunction isImageDoc(doc: FileDoc | undefined): boolean {\n if (doc?.[\"@patchwork\"]?.type !== \"file\") return false;\n if (!(doc.content instanceof Uint8Array)) return false;\n return (\n !!doc.mimeType?.startsWith(\"image/\") ||\n IMAGE_EXTENSIONS.includes(doc.extension?.toLowerCase() ?? \"\")\n );\n}\n\n/**\n * What each embedded doc turned out to be, kept for the life of the page.\n *\n * CodeMirror throws a line's DOM away when it scrolls out of view and calls\n * `toDOM` again when it comes back. Waiting on `repo.find` each time would\n * remount the embed at 0px and grow it a moment later, jumping the text\n * (scrolling up, the growth lands above what you're reading). With this the\n * remount is drawn at once, at its last size. The object urls stay alive with\n * the entry, so a remounted `<img>` needs no re-decode of the bytes into a\n * blob either.\n */\ntype Seen =\n | { kind: \"frame\" }\n | {\n kind: \"image\";\n content: Uint8Array;\n url: string;\n name: string;\n width?: number;\n height?: number;\n };\nconst seen = new Map<DocumentId, Seen>();\n\ntype ImageSeen = Extract<Seen, { kind: \"image\" }>;\n\n/** The cached image for this doc, made fresh if its bytes changed. */\nfunction imageFor(docId: DocumentId, doc: FileDoc): ImageSeen {\n const prev = seen.get(docId);\n if (prev?.kind === \"image\" && prev.content === doc.content) return prev;\n if (prev?.kind === \"image\") URL.revokeObjectURL(prev.url);\n const content = doc.content as Uint8Array;\n const next: ImageSeen = {\n kind: \"image\",\n content,\n url: URL.createObjectURL(\n new Blob([content as BlobPart], { type: doc.mimeType || \"\" })\n ),\n name: doc.name ?? \"\",\n };\n seen.set(docId, next);\n return next;\n}\n\n/**\n * Draw an image doc as a bare `<img>` that follows the doc's content; anything\n * else (or no `window.repo`) gets the normal embed frame. A doc seen before is\n * drawn straight away from `seen`. Returns a cleanup for when the widget goes.\n *\n * Reads through the realm-local `window.repo`, so an image changed only in a\n * draft shows its main-branch bytes here.\n */\nfunction mountImageOrFrame(\n docId: DocumentId,\n container: HTMLElement,\n mountFrame: () => void\n): () => void {\n const known = seen.get(docId);\n if (known?.kind === \"frame\") {\n mountFrame();\n return () => {};\n }\n\n const repo = (window as unknown as { repo?: any }).repo;\n let disposed = false;\n let handle: any;\n let img: HTMLImageElement | undefined;\n\n const show = (image: ImageSeen) => {\n if (!img) {\n container.classList.add(\"cm-embed-image\");\n img = document.createElement(\"img\");\n // Width/height attributes give the box its aspect ratio before the\n // image decodes (the theme keeps `height: auto`).\n img.addEventListener(\"load\", () => {\n const current = seen.get(docId);\n if (current?.kind !== \"image\" || img!.src !== current.url) return;\n current.width = img!.naturalWidth;\n current.height = img!.naturalHeight;\n img!.width = current.width;\n img!.height = current.height;\n });\n container.appendChild(img);\n }\n if (img.getAttribute(\"src\") === image.url) return;\n if (image.width && image.height) {\n img.width = image.width;\n img.height = image.height;\n } else {\n img.removeAttribute(\"width\");\n img.removeAttribute(\"height\");\n }\n img.alt = image.name;\n img.src = image.url;\n };\n\n const render = () => {\n const doc = handle.doc() as FileDoc | undefined;\n if (isImageDoc(doc)) show(imageFor(docId, doc!));\n };\n\n if (known?.kind === \"image\") show(known);\n\n if (!repo?.find) {\n if (!img) mountFrame();\n return () => {};\n }\n\n Promise.resolve(repo.find(`automerge:${docId}`)).then(\n (h) => {\n if (disposed) return;\n handle = h;\n const doc = handle.doc();\n if (!isImageDoc(doc)) {\n if (doc) seen.set(docId, { kind: \"frame\" });\n img?.remove();\n container.classList.remove(\"cm-embed-image\");\n mountFrame();\n return;\n }\n render();\n handle.on(\"change\", render);\n },\n () => {\n if (!disposed && !img) mountFrame();\n }\n );\n\n return () => {\n disposed = true;\n handle?.off?.(\"change\", render);\n };\n}\n\n// Marker syntax: [patchwork:docId] or [patchwork:docId/toolId] (ids can't\n// contain `/` or `]`). We scan plain text instead of the markdown syntax tree\n// on purpose: `@codemirror/language` isn't a shared singleton across\n// patchwork's separately-bundled extensions, so `syntaxTree(state)` here would\n// read a different `Language` facet and always come back empty.\nconst EMBED_PATTERN = /\\[patchwork:([^/\\]]+)(?:\\/([^\\]]+))?\\]/g;\n\nfunction getEmbedLinks(view: EditorView) {\n const widgets: Range<Decoration>[] = [];\n const { state } = view;\n\n // Visible ranges are line-aligned and markers never span lines, so a marker\n // is always fully inside or fully outside a range.\n for (const { from, to } of view.visibleRanges) {\n const text = state.doc.sliceString(from, to);\n EMBED_PATTERN.lastIndex = 0;\n let m: RegExpExecArray | null;\n while ((m = EMBED_PATTERN.exec(text)) !== null) {\n const matchFrom = from + m.index;\n const matchTo = matchFrom + m[0].length;\n const [matchText, docId, toolId] = m;\n\n if (!isValidDocumentId(docId)) continue;\n\n const embed = Decoration.replace({\n widget: new EmbedWidget(docId as DocumentId, toolId ?? null, matchText),\n });\n widgets.push(embed.range(matchFrom, matchTo));\n }\n }\n\n // `true` lets CodeMirror sort the ranges defensively (matches are already in\n // document order, but this is cheap insurance).\n return Decoration.set(widgets, true);\n}\n\n// MIME types we accept document drags from. Mirrors the sideboard's convention.\nconst PATCHWORK_DND = \"text/x-patchwork-dnd\";\nconst PATCHWORK_URLS = \"text/x-patchwork-urls\";\n\ntype DocRef = { docId: DocumentId; toolId: string | null };\n\n/** Turn an automerge url or a patchwork web link into a DocumentId, or null. */\nfunction urlToDocId(raw: string): DocumentId | null {\n const trimmed = raw.trim();\n if (!trimmed) return null;\n // Plain automerge url (optionally carrying heads/query/subpath).\n const am = trimmed.match(/automerge:([a-zA-Z0-9]+)/);\n if (am && isValidDocumentId(am[1])) return am[1] as DocumentId;\n // Patchwork web link: #doc=<documentId> (also &doc= / ?doc=).\n const web = trimmed.match(/[#&?]doc=([a-zA-Z0-9]+)/);\n if (web && isValidDocumentId(web[1])) return web[1] as DocumentId;\n return null;\n}\n\n/**\n * Read dragged documents out of a drop event, in order of format preference.\n * Only an *explicit* `toolId` on a structured item pins the tool; otherwise we\n * embed tool-less and let the view fall back to the datatype's default tool.\n */\nfunction extractDocRefs(dt: DataTransfer): DocRef[] {\n const refs: DocRef[] = [];\n const seen = new Set<string>();\n const push = (docId: DocumentId | null, toolId: string | null) => {\n if (!docId || seen.has(docId)) return;\n seen.add(docId);\n refs.push({ docId, toolId });\n };\n\n const dnd = dt.getData(PATCHWORK_DND);\n if (dnd) {\n try {\n const parsed = JSON.parse(dnd) as {\n items?: Array<{ url?: string; toolId?: string }>;\n };\n for (const item of parsed?.items ?? []) {\n if (item?.url) push(urlToDocId(item.url), item.toolId ?? null);\n }\n } catch {\n // fall through to the other formats\n }\n }\n if (refs.length > 0) return refs;\n\n const urls = dt.getData(PATCHWORK_URLS);\n if (urls) {\n try {\n const parsed: unknown = JSON.parse(urls);\n if (Array.isArray(parsed)) {\n for (const u of parsed) push(urlToDocId(String(u)), null);\n }\n } catch {\n // fall through\n }\n }\n if (refs.length > 0) return refs;\n\n const text = dt.getData(\"text/uri-list\") || dt.getData(\"text/plain\");\n if (text) {\n for (const line of text.split(/\\r?\\n/)) {\n if (line.startsWith(\"#\")) continue; // uri-list comments\n push(urlToDocId(line), null);\n }\n }\n return refs;\n}\n\nfunction embedSyntax({ docId, toolId }: DocRef): string {\n return toolId ? `[patchwork:${docId}/${toolId}]` : `[patchwork:${docId}]`;\n}\n\n/**\n * Import each OS file (dropped from the desktop or pasted) as a Patchwork\n * `file` document, returning tool-less refs to embed. Uses the realm-local\n * `window.repo` (the documented global) \u2014 fine for creating brand-new docs,\n * which aren't subject to draft remapping.\n */\nasync function importFileRefs(files: Iterable<File>): Promise<DocRef[]> {\n const repo = (window as unknown as { repo?: any }).repo;\n if (!repo) {\n console.warn(\"[codemirror-embed] window.repo unavailable; ignoring files\");\n return [];\n }\n const refs: DocRef[] = [];\n for (const file of files) {\n try {\n const mimeType = file.type || \"application/octet-stream\";\n const isText =\n mimeType.startsWith(\"text/\") || mimeType === \"application/json\";\n const content = isText\n ? await file.text()\n : new Uint8Array(await file.arrayBuffer());\n const parts = file.name.split(\".\");\n const extension = parts.length > 1 ? parts.pop()! : \"\";\n const handle = repo.create();\n handle.change((d: any) => {\n d[\"@patchwork\"] = { type: \"file\" };\n d.content = content;\n d.mimeType = mimeType;\n d.extension = extension;\n d.name = file.name;\n });\n const { documentId } = parseAutomergeUrl(handle.url as AutomergeUrl);\n if (isValidDocumentId(documentId)) {\n refs.push({ docId: documentId as DocumentId, toolId: null });\n }\n } catch (err) {\n console.warn(\n \"[codemirror-embed] failed to import file\",\n file.name,\n err\n );\n }\n }\n return refs;\n}\n\n/** Insert embeds over `from..to` (a drop point, or a pasted-over selection). */\nfunction insertRefs(\n view: EditorView,\n from: number,\n refs: DocRef[],\n to = from\n): void {\n if (refs.length === 0) return;\n // Files import asynchronously, so the doc may have shrunk in the meantime.\n const len = view.state.doc.length;\n from = Math.min(from, len);\n to = Math.min(to, len);\n let text = refs.map(embedSyntax).join(\"\\n\\n\");\n // On a non-empty line, add just enough breaks to put the embed on its own line.\n const { doc } = view.state;\n const start = doc.lineAt(from);\n const end = doc.lineAt(to);\n if (/\\S/.test(start.text) && from > start.from) text = \"\\n\" + text;\n if (/\\S/.test(end.text) && to < end.to) text = text + \"\\n\";\n view.dispatch({\n changes: { from, to, insert: text },\n selection: { anchor: from + text.length },\n });\n}\n\n/**\n * Drop handler that accepts:\n * - documents dragged from the sidebar / canvases / other tools\n * (`text/x-patchwork-dnd`, `text/x-patchwork-urls`, `text/uri-list`, and\n * `text/plain` patchwork links), and\n * - files dragged in from the operating system (imported as `file` docs).\n * Inserts `[patchwork:docId]` (or `[patchwork:docId/toolId]`) at the drop point.\n */\nfunction embedDropHandlers() {\n // Only claim the *dragover* for unambiguous patchwork/OS drags \u2014 plain\n // text/uri-list can be an ordinary in-editor text drag, which we must not\n // swallow. (`Files` covers OS drags, whose `dt.files` is empty until drop.)\n const wantsDragover = (dt: DataTransfer | null): boolean =>\n !!dt &&\n (dt.types.includes(\"Files\") ||\n dt.types.includes(PATCHWORK_DND) ||\n dt.types.includes(PATCHWORK_URLS));\n\n return EditorView.domEventHandlers({\n dragover(event) {\n const dt = event.dataTransfer;\n if (!wantsDragover(dt)) return false;\n event.preventDefault();\n // Doc drags insert a *reference* to the same automerge doc, so show the\n // link cursor; OS file drags genuinely copy content into new docs.\n if (dt) dt.dropEffect = dt.types.includes(\"Files\") ? \"copy\" : \"link\";\n return true;\n },\n drop(event, view) {\n const dt = event.dataTransfer;\n if (!dt) return false;\n const pos = view.posAtCoords({ x: event.clientX, y: event.clientY });\n if (pos == null) return false;\n\n // OS files import asynchronously; insert once the docs exist.\n if (dt.files && dt.files.length > 0) {\n event.preventDefault();\n void importFileRefs(Array.from(dt.files)).then((refs) =>\n insertRefs(view, pos, refs)\n );\n return true;\n }\n\n // Otherwise only handle the drop if it actually resolves to patchwork\n // docs (dnd/urls always do; uri-list/plain only for patchwork links).\n // If not, let CodeMirror handle it as a normal text drop.\n const refs = extractDocRefs(dt);\n if (refs.length === 0) return false;\n event.preventDefault();\n insertRefs(view, pos, refs);\n return true;\n },\n paste(event, view) {\n // Image pastes (screenshots, \"Copy Image\") become embedded `file` docs.\n // A paste that also carries text is left to CodeMirror: rich text from\n // office apps attaches a picture of itself, and a Finder copy carries\n // the file's icon alongside its name.\n const dt = event.clipboardData;\n if (!dt || dt.getData(\"text/plain\")) return false;\n const images = Array.from(dt.files ?? []).filter((f) =>\n f.type.startsWith(\"image/\")\n );\n if (images.length === 0) return false;\n event.preventDefault();\n const { from, to } = view.state.selection.main;\n void importFileRefs(images).then((refs) =>\n insertRefs(view, from, refs, to)\n );\n return true;\n },\n });\n}\n\nconst embedPlugin = ViewPlugin.fromClass(\n class {\n decorations: DecorationSet;\n\n constructor(view: EditorView) {\n this.decorations = getEmbedLinks(view);\n }\n\n update(update: ViewUpdate) {\n // Viewport too, so newly-visible markers get decorated.\n if (update.docChanged || update.viewportChanged) {\n this.decorations = getEmbedLinks(update.view);\n }\n }\n },\n {\n decorations: (v) => v.decorations,\n // Atomic: the cursor skips embeds and delete removes the whole marker.\n provide: (plugin) =>\n EditorView.atomicRanges.of(\n (view) => view.plugin(plugin)?.decorations ?? Decoration.none\n ),\n }\n);\n\nexport function markdownEmbed() {\n // `dropCursor` observes events, so it coexists with our dragover handler.\n return [embedPlugin, embedTheme, embedDropHandlers(), dropCursor()];\n}\n", "import { EditorView } from \"@codemirror/view\";\n\n// Outer container only; all embed chrome lives in the shared \"embed\" tool.\nexport const embedTheme = EditorView.baseTheme({\n \".cm-embed\": {\n display: \"block\",\n border: \"var(--studio-border-width, 1px) var(--studio-border-style, solid)\",\n borderRadius: \"4px\",\n overflow: \"hidden\",\n },\n \"&light .cm-embed\": {\n borderColor: \"#ddd\",\n },\n \"&dark .cm-embed\": {\n borderColor: \"#333\",\n },\n // Images are just images: no border, natural size, never wider than the text.\n \"&light .cm-embed.cm-embed-image, &dark .cm-embed.cm-embed-image\": {\n border: \"none\",\n borderRadius: \"0\",\n },\n \".cm-embed-image > img\": {\n display: \"block\",\n maxWidth: \"100%\",\n height: \"auto\",\n },\n \".cm-embed > patchwork-view\": {\n display: \"block\",\n height: \"500px\",\n width: \"100%\",\n },\n});\n"],
5
+ "mappings": "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",
6
+ "names": ["EditorView", "seen", "EditorView", "refs"]
7
7
  }
@@ -5,6 +5,10 @@ import {
5
5
  WidgetType,
6
6
  ViewPlugin
7
7
  } from "@codemirror/view";
8
+ import {
9
+ StateEffect,
10
+ StateField
11
+ } from "@codemirror/state";
8
12
  import { syntaxTree } from "@codemirror/language";
9
13
  import { isValidAutomergeUrl } from "@automerge/automerge-repo/slim";
10
14
 
@@ -20,7 +24,7 @@ var linkTheme = EditorView.baseTheme({
20
24
  color: "inherit"
21
25
  }
22
26
  },
23
- "&.cm-mod-held .cm-md-link, &.cm-mod-held .cm-md-link *": {
27
+ ".cm-md-link-hover, .cm-md-link-hover *": {
24
28
  textDecoration: "underline",
25
29
  cursor: "var(--studio-cursor-pointer, pointer)"
26
30
  }
@@ -115,92 +119,105 @@ var linkPlugin = ViewPlugin.fromClass(
115
119
  decorations: (v) => v.decorations
116
120
  }
117
121
  );
118
- function urlAt(state, pos) {
122
+ function linkAt(state, pos) {
123
+ const slice = (n) => state.doc.sliceString(n.from, n.to);
119
124
  for (const side of [1, -1]) {
120
125
  for (let node = syntaxTree(state).resolveInner(pos, side); node; node = node.parent) {
121
- if (node.name === "URL") return state.doc.sliceString(node.from, node.to);
126
+ const inLink = node.parent?.name === "Link" || node.parent?.name === "Autolink";
127
+ if (node.name === "URL" && !inLink)
128
+ return { from: node.from, to: node.to, url: slice(node) };
122
129
  if (node.name === "Link" || node.name === "Autolink") {
123
130
  const url = node.getChild("URL");
124
- return url ? state.doc.sliceString(url.from, url.to) : null;
131
+ return url ? { from: node.from, to: node.to, url: slice(url) } : null;
125
132
  }
126
133
  }
127
134
  }
128
135
  return null;
129
136
  }
137
+ var urlAt = (state, pos) => linkAt(state, pos)?.url ?? null;
138
+ function linkUnderPointer(view, x, y) {
139
+ const hit = view.posAndSideAtCoords({ x, y });
140
+ if (!hit) return null;
141
+ const at = view.coordsAtPos(hit.pos, hit.assoc);
142
+ if (!at || y < at.top || y > at.bottom || Math.abs(x - at.left) > view.defaultCharacterWidth * 1.5)
143
+ return null;
144
+ return linkAt(view.state, hit.pos);
145
+ }
130
146
  var isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/.test(navigator.platform || navigator.userAgent);
131
147
  var modHeld = (e) => isMac ? e.metaKey : e.ctrlKey;
132
- var linkMark = Decoration.mark({ class: "cm-md-link" });
133
- var linkMarks = ViewPlugin.fromClass(
148
+ var setHover = StateEffect.define();
149
+ var hoverMark = Decoration.mark({ class: "cm-md-link-hover" });
150
+ var hoverField = StateField.define({
151
+ create: () => Decoration.none,
152
+ update(deco, tr) {
153
+ deco = deco.map(tr.changes);
154
+ for (const e of tr.effects) {
155
+ if (e.is(setHover)) {
156
+ deco = e.value ? Decoration.set(hoverMark.range(e.value.from, e.value.to)) : Decoration.none;
157
+ }
158
+ }
159
+ return deco;
160
+ },
161
+ provide: (f) => EditorView2.decorations.from(f)
162
+ });
163
+ var modClick = ViewPlugin.fromClass(
134
164
  class {
135
- decorations;
136
165
  constructor(view) {
137
- this.decorations = this.build(view);
166
+ this.view = view;
138
167
  }
139
- update(update) {
140
- if (update.docChanged || update.viewportChanged || syntaxTree(update.startState) !== syntaxTree(update.state)) {
141
- this.decorations = this.build(update.view);
142
- }
168
+ pointer = null;
169
+ mod = false;
170
+ refresh() {
171
+ const { view } = this;
172
+ const link = this.mod && this.pointer ? linkUnderPointer(view, this.pointer.x, this.pointer.y) : null;
173
+ let current = null;
174
+ view.state.field(hoverField).between(0, view.state.doc.length, (from, to) => {
175
+ current = { from, to };
176
+ });
177
+ const same = (a, b) => a?.from === b?.from && a?.to === b?.to;
178
+ const next = link && { from: link.from, to: link.to };
179
+ if (!same(current, next)) view.dispatch({ effects: setHover.of(next) });
143
180
  }
144
- build(view) {
145
- const marks = [];
146
- for (const { from, to } of view.visibleRanges) {
147
- syntaxTree(view.state).iterate({
148
- from,
149
- to,
150
- enter: (node) => {
151
- const name = node.name;
152
- if (name === "Link" || name === "Autolink") {
153
- if (node.node.getChild("URL"))
154
- marks.push(linkMark.range(node.from, node.to));
155
- return false;
156
- }
157
- if (name === "URL") marks.push(linkMark.range(node.from, node.to));
158
- }
159
- });
181
+ },
182
+ {
183
+ eventHandlers: {
184
+ mousedown(event, view) {
185
+ if (event.button !== 0 || !modHeld(event)) return false;
186
+ const link = linkUnderPointer(view, event.clientX, event.clientY);
187
+ if (!link) return false;
188
+ event.preventDefault();
189
+ openUrl(link.url, view.dom);
190
+ return true;
191
+ },
192
+ mousemove(event) {
193
+ this.pointer = { x: event.clientX, y: event.clientY };
194
+ this.mod = modHeld(event);
195
+ this.refresh();
196
+ },
197
+ mouseleave() {
198
+ this.pointer = null;
199
+ this.refresh();
200
+ },
201
+ keydown(event) {
202
+ this.mod = modHeld(event);
203
+ this.refresh();
204
+ },
205
+ keyup(event) {
206
+ this.mod = modHeld(event);
207
+ this.refresh();
208
+ },
209
+ blur() {
210
+ this.mod = false;
211
+ this.refresh();
160
212
  }
161
- return Decoration.set(marks, true);
162
213
  }
163
- },
164
- { decorations: (v) => v.decorations }
214
+ }
165
215
  );
166
- var modClick = [
167
- EditorView2.domEventHandlers({
168
- mousedown(event, view) {
169
- if (event.button !== 0 || !modHeld(event)) return false;
170
- const pos = view.posAtCoords({ x: event.clientX, y: event.clientY });
171
- if (pos == null) return false;
172
- const url = urlAt(view.state, pos);
173
- if (!url) return false;
174
- event.preventDefault();
175
- openUrl(url, view.dom);
176
- return true;
177
- },
178
- keydown(event, view) {
179
- view.dom.classList.toggle("cm-mod-held", modHeld(event));
180
- return false;
181
- },
182
- keyup(event, view) {
183
- view.dom.classList.toggle("cm-mod-held", modHeld(event));
184
- return false;
185
- },
186
- mousemove(event, view) {
187
- view.dom.classList.toggle("cm-mod-held", modHeld(event));
188
- return false;
189
- },
190
- mouseleave(_event, view) {
191
- view.dom.classList.remove("cm-mod-held");
192
- return false;
193
- },
194
- blur(_event, view) {
195
- view.dom.classList.remove("cm-mod-held");
196
- return false;
197
- }
198
- })
199
- ];
200
216
  function markdownLinks() {
201
- return [linkPlugin, linkMarks, modClick, linkTheme];
217
+ return [linkPlugin, hoverField, modClick, linkTheme];
202
218
  }
203
219
  export {
220
+ linkAt,
204
221
  markdownLinks,
205
222
  openUrl,
206
223
  urlAt
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/extensions/links.ts", "../../src/themes/links.ts"],
4
- "sourcesContent": ["import {\n EditorView,\n Decoration,\n WidgetType,\n ViewPlugin,\n ViewUpdate,\n type DecorationSet,\n} from \"@codemirror/view\";\nimport { type EditorState, Range } from \"@codemirror/state\";\nimport { syntaxTree } from \"@codemirror/language\";\nimport { isValidAutomergeUrl } from \"@automerge/automerge-repo/slim\";\nimport { linkTheme } from \"../themes/links.ts\";\n\n/**\n * Open a link from the editor. Automerge urls open inside Patchwork rather\n * than as a browser URL (automerge: isn't navigable); bubbles/composed so the\n * event escapes the editor and any shadow root up to whoever drives selection.\n */\nexport function openUrl(url: string, from: Element) {\n if (isValidAutomergeUrl(url)) {\n from.dispatchEvent(\n new CustomEvent(\"patchwork:open-document\", {\n detail: { url },\n bubbles: true,\n composed: true,\n }),\n );\n } else {\n // GFM autolinks `www.example.com` with no scheme.\n const href = /^[a-z][a-z0-9+.-]*:/i.test(url) ? url : `https://${url}`;\n window.open(href, \"_blank\", \"noopener,noreferrer\");\n }\n}\n\n/**\n * Widget to render a clickable link in the editor\n */\nclass LinkWidget extends WidgetType {\n readonly url: string;\n readonly text: string;\n\n constructor(url: string, text: string) {\n super();\n this.url = url;\n this.text = text;\n }\n\n eq(other: LinkWidget) {\n return other.url === this.url && other.text === this.text;\n }\n\n toDOM() {\n const link = document.createElement(\"a\");\n link.href = this.url;\n link.textContent = this.text;\n link.className = \"cm-link\";\n link.title = this.url;\n\n link.onclick = (e) => {\n e.preventDefault();\n openUrl(this.url, link);\n };\n\n return link;\n }\n\n ignoreEvent(e: Event) {\n // Let click events through to our widget\n return e.type !== \"click\";\n }\n}\n\nfunction getLinks(view: EditorView) {\n const widgets: Range<Decoration>[] = [];\n const { state } = view;\n const selection = state.selection.main;\n\n for (let { from, to } of view.visibleRanges) {\n syntaxTree(state).iterate({\n from,\n to,\n enter: (node) => {\n // Match markdown link syntax\n if (node.name === \"Link\") {\n const linkFrom = node.from;\n const linkTo = node.to;\n\n // Check if cursor is inside this link\n const cursorInLink =\n selection.from >= linkFrom && selection.from <= linkTo;\n // Check if the link is inside the selection\n const selectionSpansLink =\n selection.from < linkFrom && selection.to > linkTo;\n\n // Replace if cursor is outside the link and there's no selection across the link\n if (!cursorInLink && !selectionSpansLink) {\n const linkText = state.doc.sliceString(linkFrom, linkTo);\n const match = linkText.match(/\\[([^\\]]+)\\]\\(([^)]+)\\)/);\n\n if (match) {\n const [, text, url] = match;\n\n // Replace the entire [text](url) with just the link widget\n const deco = Decoration.replace({\n widget: new LinkWidget(url, text),\n });\n\n widgets.push(deco.range(linkFrom, linkTo));\n }\n }\n }\n },\n });\n }\n\n return Decoration.set(widgets);\n}\n\nconst linkPlugin = ViewPlugin.fromClass(\n class {\n decorations: DecorationSet;\n\n constructor(view: EditorView) {\n this.decorations = getLinks(view);\n }\n\n update(update: ViewUpdate) {\n // Recompute when doc changes, selection moves, or viewport changes\n if (\n update.docChanged ||\n update.selectionSet ||\n update.viewportChanged ||\n syntaxTree(update.startState) !== syntaxTree(update.state)\n ) {\n this.decorations = getLinks(update.view);\n }\n }\n },\n {\n decorations: (v) => v.decorations,\n }\n);\n\ntype SyntaxNode = ReturnType<ReturnType<typeof syntaxTree>[\"resolveInner\"]>;\n\n/**\n * The url of the link at `pos`, if any: an inline `[text](url)`, an autolink\n * `<url>`, a GFM bare url, or the url in a `[ref]: url` definition.\n * Reference-style `[text][ref]` links have no url of their own and are skipped.\n */\nexport function urlAt(state: EditorState, pos: number): string | null {\n for (const side of [1, -1] as const) {\n for (\n let node: SyntaxNode | null = syntaxTree(state).resolveInner(pos, side);\n node;\n node = node.parent\n ) {\n if (node.name === \"URL\") return state.doc.sliceString(node.from, node.to);\n if (node.name === \"Link\" || node.name === \"Autolink\") {\n const url = node.getChild(\"URL\");\n return url ? state.doc.sliceString(url.from, url.to) : null;\n }\n }\n }\n return null;\n}\n\nconst isMac =\n typeof navigator !== \"undefined\" &&\n /Mac|iPhone|iPad|iPod/.test(navigator.platform || navigator.userAgent);\nconst modHeld = (e: KeyboardEvent | MouseEvent) =>\n isMac ? e.metaKey : e.ctrlKey;\n\n/** Marks link text so it can look clickable while Mod is held. */\nconst linkMark = Decoration.mark({ class: \"cm-md-link\" });\n\nconst linkMarks = ViewPlugin.fromClass(\n class {\n decorations: DecorationSet;\n constructor(view: EditorView) {\n this.decorations = this.build(view);\n }\n update(update: ViewUpdate) {\n if (\n update.docChanged ||\n update.viewportChanged ||\n syntaxTree(update.startState) !== syntaxTree(update.state)\n ) {\n this.decorations = this.build(update.view);\n }\n }\n build(view: EditorView) {\n const marks: Range<Decoration>[] = [];\n for (const { from, to } of view.visibleRanges) {\n syntaxTree(view.state).iterate({\n from,\n to,\n enter: (node) => {\n const name = node.name;\n if (name === \"Link\" || name === \"Autolink\") {\n if (node.node.getChild(\"URL\"))\n marks.push(linkMark.range(node.from, node.to));\n return false;\n }\n if (name === \"URL\") marks.push(linkMark.range(node.from, node.to));\n },\n });\n }\n return Decoration.set(marks, true);\n }\n },\n { decorations: (v) => v.decorations },\n);\n\n/**\n * Cmd-click (Ctrl off a Mac) opens the link under the pointer, even while the\n * cursor is inside it and it's showing as raw markdown. Holding the key puts\n * `cm-mod-held` on the editor so the theme can show links as clickable.\n */\nconst modClick = [\n EditorView.domEventHandlers({\n mousedown(event, view) {\n if (event.button !== 0 || !modHeld(event)) return false;\n const pos = view.posAtCoords({ x: event.clientX, y: event.clientY });\n if (pos == null) return false;\n const url = urlAt(view.state, pos);\n if (!url) return false;\n event.preventDefault();\n openUrl(url, view.dom);\n return true;\n },\n keydown(event, view) {\n view.dom.classList.toggle(\"cm-mod-held\", modHeld(event));\n return false;\n },\n keyup(event, view) {\n view.dom.classList.toggle(\"cm-mod-held\", modHeld(event));\n return false;\n },\n mousemove(event, view) {\n view.dom.classList.toggle(\"cm-mod-held\", modHeld(event));\n return false;\n },\n mouseleave(_event, view) {\n view.dom.classList.remove(\"cm-mod-held\");\n return false;\n },\n blur(_event, view) {\n view.dom.classList.remove(\"cm-mod-held\");\n return false;\n },\n }),\n];\n\nexport function markdownLinks() {\n return [linkPlugin, linkMarks, modClick, linkTheme];\n}\n", "import { EditorView } from \"@codemirror/view\";\n\nexport const linkTheme = EditorView.baseTheme({\n \".cm-link\": {\n textDecoration: \"underline\",\n cursor: \"var(--studio-cursor-pointer, pointer)\",\n fontFamily: '\"Merriweather Sans\", sans-serif',\n color: \"var(--syntax-link, #0066cc)\",\n \"&:hover\": {\n color: \"inherit\",\n },\n },\n \"&.cm-mod-held .cm-md-link, &.cm-mod-held .cm-md-link *\": {\n textDecoration: \"underline\",\n cursor: \"var(--studio-cursor-pointer, pointer)\",\n },\n});\n"],
5
- "mappings": ";AAAA;AAAA,EACE,cAAAA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAEP,SAAS,kBAAkB;AAC3B,SAAS,2BAA2B;;;ACVpC,SAAS,kBAAkB;AAEpB,IAAM,YAAY,WAAW,UAAU;AAAA,EAC5C,YAAY;AAAA,IACV,gBAAgB;AAAA,IAChB,QAAQ;AAAA,IACR,YAAY;AAAA,IACZ,OAAO;AAAA,IACP,WAAW;AAAA,MACT,OAAO;AAAA,IACT;AAAA,EACF;AAAA,EACA,0DAA0D;AAAA,IACxD,gBAAgB;AAAA,IAChB,QAAQ;AAAA,EACV;AACF,CAAC;;;ADEM,SAAS,QAAQ,KAAa,MAAe;AAClD,MAAI,oBAAoB,GAAG,GAAG;AAC5B,SAAK;AAAA,MACH,IAAI,YAAY,2BAA2B;AAAA,QACzC,QAAQ,EAAE,IAAI;AAAA,QACd,SAAS;AAAA,QACT,UAAU;AAAA,MACZ,CAAC;AAAA,IACH;AAAA,EACF,OAAO;AAEL,UAAM,OAAO,uBAAuB,KAAK,GAAG,IAAI,MAAM,WAAW,GAAG;AACpE,WAAO,KAAK,MAAM,UAAU,qBAAqB;AAAA,EACnD;AACF;AAKA,IAAM,aAAN,cAAyB,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EAET,YAAY,KAAa,MAAc;AACrC,UAAM;AACN,SAAK,MAAM;AACX,SAAK,OAAO;AAAA,EACd;AAAA,EAEA,GAAG,OAAmB;AACpB,WAAO,MAAM,QAAQ,KAAK,OAAO,MAAM,SAAS,KAAK;AAAA,EACvD;AAAA,EAEA,QAAQ;AACN,UAAM,OAAO,SAAS,cAAc,GAAG;AACvC,SAAK,OAAO,KAAK;AACjB,SAAK,cAAc,KAAK;AACxB,SAAK,YAAY;AACjB,SAAK,QAAQ,KAAK;AAElB,SAAK,UAAU,CAAC,MAAM;AACpB,QAAE,eAAe;AACjB,cAAQ,KAAK,KAAK,IAAI;AAAA,IACxB;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,YAAY,GAAU;AAEpB,WAAO,EAAE,SAAS;AAAA,EACpB;AACF;AAEA,SAAS,SAAS,MAAkB;AAClC,QAAM,UAA+B,CAAC;AACtC,QAAM,EAAE,MAAM,IAAI;AAClB,QAAM,YAAY,MAAM,UAAU;AAElC,WAAS,EAAE,MAAM,GAAG,KAAK,KAAK,eAAe;AAC3C,eAAW,KAAK,EAAE,QAAQ;AAAA,MACxB;AAAA,MACA;AAAA,MACA,OAAO,CAAC,SAAS;AAEf,YAAI,KAAK,SAAS,QAAQ;AACxB,gBAAM,WAAW,KAAK;AACtB,gBAAM,SAAS,KAAK;AAGpB,gBAAM,eACJ,UAAU,QAAQ,YAAY,UAAU,QAAQ;AAElD,gBAAM,qBACJ,UAAU,OAAO,YAAY,UAAU,KAAK;AAG9C,cAAI,CAAC,gBAAgB,CAAC,oBAAoB;AACxC,kBAAM,WAAW,MAAM,IAAI,YAAY,UAAU,MAAM;AACvD,kBAAM,QAAQ,SAAS,MAAM,yBAAyB;AAEtD,gBAAI,OAAO;AACT,oBAAM,CAAC,EAAE,MAAM,GAAG,IAAI;AAGtB,oBAAM,OAAO,WAAW,QAAQ;AAAA,gBAC9B,QAAQ,IAAI,WAAW,KAAK,IAAI;AAAA,cAClC,CAAC;AAED,sBAAQ,KAAK,KAAK,MAAM,UAAU,MAAM,CAAC;AAAA,YAC3C;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AAEA,SAAO,WAAW,IAAI,OAAO;AAC/B;AAEA,IAAM,aAAa,WAAW;AAAA,EAC5B,MAAM;AAAA,IACJ;AAAA,IAEA,YAAY,MAAkB;AAC5B,WAAK,cAAc,SAAS,IAAI;AAAA,IAClC;AAAA,IAEA,OAAO,QAAoB;AAEzB,UACE,OAAO,cACP,OAAO,gBACP,OAAO,mBACP,WAAW,OAAO,UAAU,MAAM,WAAW,OAAO,KAAK,GACzD;AACA,aAAK,cAAc,SAAS,OAAO,IAAI;AAAA,MACzC;AAAA,IACF;AAAA,EACF;AAAA,EACA;AAAA,IACE,aAAa,CAAC,MAAM,EAAE;AAAA,EACxB;AACF;AASO,SAAS,MAAM,OAAoB,KAA4B;AACpE,aAAW,QAAQ,CAAC,GAAG,EAAE,GAAY;AACnC,aACM,OAA0B,WAAW,KAAK,EAAE,aAAa,KAAK,IAAI,GACtE,MACA,OAAO,KAAK,QACZ;AACA,UAAI,KAAK,SAAS,MAAO,QAAO,MAAM,IAAI,YAAY,KAAK,MAAM,KAAK,EAAE;AACxE,UAAI,KAAK,SAAS,UAAU,KAAK,SAAS,YAAY;AACpD,cAAM,MAAM,KAAK,SAAS,KAAK;AAC/B,eAAO,MAAM,MAAM,IAAI,YAAY,IAAI,MAAM,IAAI,EAAE,IAAI;AAAA,MACzD;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAEA,IAAM,QACJ,OAAO,cAAc,eACrB,uBAAuB,KAAK,UAAU,YAAY,UAAU,SAAS;AACvE,IAAM,UAAU,CAAC,MACf,QAAQ,EAAE,UAAU,EAAE;AAGxB,IAAM,WAAW,WAAW,KAAK,EAAE,OAAO,aAAa,CAAC;AAExD,IAAM,YAAY,WAAW;AAAA,EAC3B,MAAM;AAAA,IACJ;AAAA,IACA,YAAY,MAAkB;AAC5B,WAAK,cAAc,KAAK,MAAM,IAAI;AAAA,IACpC;AAAA,IACA,OAAO,QAAoB;AACzB,UACE,OAAO,cACP,OAAO,mBACP,WAAW,OAAO,UAAU,MAAM,WAAW,OAAO,KAAK,GACzD;AACA,aAAK,cAAc,KAAK,MAAM,OAAO,IAAI;AAAA,MAC3C;AAAA,IACF;AAAA,IACA,MAAM,MAAkB;AACtB,YAAM,QAA6B,CAAC;AACpC,iBAAW,EAAE,MAAM,GAAG,KAAK,KAAK,eAAe;AAC7C,mBAAW,KAAK,KAAK,EAAE,QAAQ;AAAA,UAC7B;AAAA,UACA;AAAA,UACA,OAAO,CAAC,SAAS;AACf,kBAAM,OAAO,KAAK;AAClB,gBAAI,SAAS,UAAU,SAAS,YAAY;AAC1C,kBAAI,KAAK,KAAK,SAAS,KAAK;AAC1B,sBAAM,KAAK,SAAS,MAAM,KAAK,MAAM,KAAK,EAAE,CAAC;AAC/C,qBAAO;AAAA,YACT;AACA,gBAAI,SAAS,MAAO,OAAM,KAAK,SAAS,MAAM,KAAK,MAAM,KAAK,EAAE,CAAC;AAAA,UACnE;AAAA,QACF,CAAC;AAAA,MACH;AACA,aAAO,WAAW,IAAI,OAAO,IAAI;AAAA,IACnC;AAAA,EACF;AAAA,EACA,EAAE,aAAa,CAAC,MAAM,EAAE,YAAY;AACtC;AAOA,IAAM,WAAW;AAAA,EACfC,YAAW,iBAAiB;AAAA,IAC1B,UAAU,OAAO,MAAM;AACrB,UAAI,MAAM,WAAW,KAAK,CAAC,QAAQ,KAAK,EAAG,QAAO;AAClD,YAAM,MAAM,KAAK,YAAY,EAAE,GAAG,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC;AACnE,UAAI,OAAO,KAAM,QAAO;AACxB,YAAM,MAAM,MAAM,KAAK,OAAO,GAAG;AACjC,UAAI,CAAC,IAAK,QAAO;AACjB,YAAM,eAAe;AACrB,cAAQ,KAAK,KAAK,GAAG;AACrB,aAAO;AAAA,IACT;AAAA,IACA,QAAQ,OAAO,MAAM;AACnB,WAAK,IAAI,UAAU,OAAO,eAAe,QAAQ,KAAK,CAAC;AACvD,aAAO;AAAA,IACT;AAAA,IACA,MAAM,OAAO,MAAM;AACjB,WAAK,IAAI,UAAU,OAAO,eAAe,QAAQ,KAAK,CAAC;AACvD,aAAO;AAAA,IACT;AAAA,IACA,UAAU,OAAO,MAAM;AACrB,WAAK,IAAI,UAAU,OAAO,eAAe,QAAQ,KAAK,CAAC;AACvD,aAAO;AAAA,IACT;AAAA,IACA,WAAW,QAAQ,MAAM;AACvB,WAAK,IAAI,UAAU,OAAO,aAAa;AACvC,aAAO;AAAA,IACT;AAAA,IACA,KAAK,QAAQ,MAAM;AACjB,WAAK,IAAI,UAAU,OAAO,aAAa;AACvC,aAAO;AAAA,IACT;AAAA,EACF,CAAC;AACH;AAEO,SAAS,gBAAgB;AAC9B,SAAO,CAAC,YAAY,WAAW,UAAU,SAAS;AACpD;",
4
+ "sourcesContent": ["import {\n EditorView,\n Decoration,\n WidgetType,\n ViewPlugin,\n ViewUpdate,\n type DecorationSet,\n} from \"@codemirror/view\";\nimport {\n type EditorState,\n Range,\n StateEffect,\n StateField,\n} from \"@codemirror/state\";\nimport { syntaxTree } from \"@codemirror/language\";\nimport { isValidAutomergeUrl } from \"@automerge/automerge-repo/slim\";\nimport { linkTheme } from \"../themes/links.ts\";\n\n/**\n * Open a link from the editor. Automerge urls open inside Patchwork rather\n * than as a browser URL (automerge: isn't navigable); bubbles/composed so the\n * event escapes the editor and any shadow root up to whoever drives selection.\n */\nexport function openUrl(url: string, from: Element) {\n if (isValidAutomergeUrl(url)) {\n from.dispatchEvent(\n new CustomEvent(\"patchwork:open-document\", {\n detail: { url },\n bubbles: true,\n composed: true,\n }),\n );\n } else {\n // GFM autolinks `www.example.com` with no scheme.\n const href = /^[a-z][a-z0-9+.-]*:/i.test(url) ? url : `https://${url}`;\n window.open(href, \"_blank\", \"noopener,noreferrer\");\n }\n}\n\n/**\n * Widget to render a clickable link in the editor\n */\nclass LinkWidget extends WidgetType {\n readonly url: string;\n readonly text: string;\n\n constructor(url: string, text: string) {\n super();\n this.url = url;\n this.text = text;\n }\n\n eq(other: LinkWidget) {\n return other.url === this.url && other.text === this.text;\n }\n\n toDOM() {\n const link = document.createElement(\"a\");\n link.href = this.url;\n link.textContent = this.text;\n link.className = \"cm-link\";\n link.title = this.url;\n\n link.onclick = (e) => {\n e.preventDefault();\n openUrl(this.url, link);\n };\n\n return link;\n }\n\n ignoreEvent(e: Event) {\n // Let click events through to our widget\n return e.type !== \"click\";\n }\n}\n\nfunction getLinks(view: EditorView) {\n const widgets: Range<Decoration>[] = [];\n const { state } = view;\n const selection = state.selection.main;\n\n for (let { from, to } of view.visibleRanges) {\n syntaxTree(state).iterate({\n from,\n to,\n enter: (node) => {\n // Match markdown link syntax\n if (node.name === \"Link\") {\n const linkFrom = node.from;\n const linkTo = node.to;\n\n // Check if cursor is inside this link\n const cursorInLink =\n selection.from >= linkFrom && selection.from <= linkTo;\n // Check if the link is inside the selection\n const selectionSpansLink =\n selection.from < linkFrom && selection.to > linkTo;\n\n // Replace if cursor is outside the link and there's no selection across the link\n if (!cursorInLink && !selectionSpansLink) {\n const linkText = state.doc.sliceString(linkFrom, linkTo);\n const match = linkText.match(/\\[([^\\]]+)\\]\\(([^)]+)\\)/);\n\n if (match) {\n const [, text, url] = match;\n\n // Replace the entire [text](url) with just the link widget\n const deco = Decoration.replace({\n widget: new LinkWidget(url, text),\n });\n\n widgets.push(deco.range(linkFrom, linkTo));\n }\n }\n }\n },\n });\n }\n\n return Decoration.set(widgets);\n}\n\nconst linkPlugin = ViewPlugin.fromClass(\n class {\n decorations: DecorationSet;\n\n constructor(view: EditorView) {\n this.decorations = getLinks(view);\n }\n\n update(update: ViewUpdate) {\n // Recompute when doc changes, selection moves, or viewport changes\n if (\n update.docChanged ||\n update.selectionSet ||\n update.viewportChanged ||\n syntaxTree(update.startState) !== syntaxTree(update.state)\n ) {\n this.decorations = getLinks(update.view);\n }\n }\n },\n {\n decorations: (v) => v.decorations,\n }\n);\n\ntype SyntaxNode = ReturnType<ReturnType<typeof syntaxTree>[\"resolveInner\"]>;\n\n/**\n * The link at `pos`, if any: an inline `[text](url)`, an autolink `<url>`, a\n * GFM bare url, or the url in a `[ref]: url` definition. `from`/`to` cover\n * the whole link as written. Reference-style `[text][ref]` links have no url\n * of their own and are skipped.\n */\nexport function linkAt(\n state: EditorState,\n pos: number\n): { from: number; to: number; url: string } | null {\n const slice = (n: SyntaxNode) => state.doc.sliceString(n.from, n.to);\n for (const side of [1, -1] as const) {\n for (\n let node: SyntaxNode | null = syntaxTree(state).resolveInner(pos, side);\n node;\n node = node.parent\n ) {\n const inLink = node.parent?.name === \"Link\" || node.parent?.name === \"Autolink\";\n if (node.name === \"URL\" && !inLink)\n return { from: node.from, to: node.to, url: slice(node) };\n if (node.name === \"Link\" || node.name === \"Autolink\") {\n const url = node.getChild(\"URL\");\n return url ? { from: node.from, to: node.to, url: slice(url) } : null;\n }\n }\n }\n return null;\n}\n\nexport const urlAt = (state: EditorState, pos: number) =>\n linkAt(state, pos)?.url ?? null;\n\n/**\n * The link actually under the pointer. `posAndSideAtCoords` snaps to the\n * nearest position, so without the distance check the blank space past the\n * end of a line would count as hovering a link that ends the line.\n */\nfunction linkUnderPointer(view: EditorView, x: number, y: number) {\n const hit = view.posAndSideAtCoords({ x, y });\n if (!hit) return null;\n const at = view.coordsAtPos(hit.pos, hit.assoc);\n if (\n !at ||\n y < at.top ||\n y > at.bottom ||\n Math.abs(x - at.left) > view.defaultCharacterWidth * 1.5\n )\n return null;\n return linkAt(view.state, hit.pos);\n}\n\nconst isMac =\n typeof navigator !== \"undefined\" &&\n /Mac|iPhone|iPad|iPod/.test(navigator.platform || navigator.userAgent);\nconst modHeld = (e: KeyboardEvent | MouseEvent) =>\n isMac ? e.metaKey : e.ctrlKey;\n\n/** The one link to show as clickable: under the pointer, with Mod held. */\nconst setHover = StateEffect.define<{ from: number; to: number } | null>();\nconst hoverMark = Decoration.mark({ class: \"cm-md-link-hover\" });\n\nconst hoverField = StateField.define<DecorationSet>({\n create: () => Decoration.none,\n update(deco, tr) {\n deco = deco.map(tr.changes);\n for (const e of tr.effects) {\n if (e.is(setHover)) {\n deco = e.value\n ? Decoration.set(hoverMark.range(e.value.from, e.value.to))\n : Decoration.none;\n }\n }\n return deco;\n },\n provide: (f) => EditorView.decorations.from(f),\n});\n\n/**\n * Cmd-click (Ctrl off a Mac) opens the link under the pointer, even while the\n * cursor is inside it and it's showing as raw markdown. While the key is held\n * the hovered link gets `cm-md-link-hover`, so the theme can underline it.\n */\nconst modClick = ViewPlugin.fromClass(\n class {\n pointer: { x: number; y: number } | null = null;\n mod = false;\n\n constructor(readonly view: EditorView) {}\n\n refresh() {\n const { view } = this;\n const link =\n this.mod && this.pointer\n ? linkUnderPointer(view, this.pointer.x, this.pointer.y)\n : null;\n let current: { from: number; to: number } | null = null;\n view.state.field(hoverField).between(0, view.state.doc.length, (from, to) => {\n current = { from, to };\n });\n const same = (a: typeof current, b: typeof current) =>\n a?.from === b?.from && a?.to === b?.to;\n const next = link && { from: link.from, to: link.to };\n if (!same(current, next)) view.dispatch({ effects: setHover.of(next) });\n }\n },\n {\n eventHandlers: {\n mousedown(event, view) {\n if (event.button !== 0 || !modHeld(event)) return false;\n const link = linkUnderPointer(view, event.clientX, event.clientY);\n if (!link) return false;\n event.preventDefault();\n openUrl(link.url, view.dom);\n return true;\n },\n mousemove(event) {\n this.pointer = { x: event.clientX, y: event.clientY };\n this.mod = modHeld(event);\n this.refresh();\n },\n mouseleave() {\n this.pointer = null;\n this.refresh();\n },\n keydown(event) {\n this.mod = modHeld(event);\n this.refresh();\n },\n keyup(event) {\n this.mod = modHeld(event);\n this.refresh();\n },\n blur() {\n this.mod = false;\n this.refresh();\n },\n },\n }\n);\n\nexport function markdownLinks() {\n return [linkPlugin, hoverField, modClick, linkTheme];\n}\n", "import { EditorView } from \"@codemirror/view\";\n\nexport const linkTheme = EditorView.baseTheme({\n \".cm-link\": {\n textDecoration: \"underline\",\n cursor: \"var(--studio-cursor-pointer, pointer)\",\n fontFamily: '\"Merriweather Sans\", sans-serif',\n color: \"var(--syntax-link, #0066cc)\",\n \"&:hover\": {\n color: \"inherit\",\n },\n },\n \".cm-md-link-hover, .cm-md-link-hover *\": {\n textDecoration: \"underline\",\n cursor: \"var(--studio-cursor-pointer, pointer)\",\n },\n});\n"],
5
+ "mappings": "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",
6
6
  "names": ["EditorView", "EditorView"]
7
7
  }
@@ -1 +1 @@
1
- automerge:QPJoYHY2ASHTf4aQY6Z4VB8tvte
1
+ automerge:3nJh3fTdVVTn5bT5BrYYdUq4hRYF