@inkandswitch/patchwork-pkg-base 0.0.121 → 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:23J7DGQRcwEus8TKfgXca2k4Roc
1
+ automerge:BQ45jRvfLwfaVSXvcbUquxQViiA
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inkandswitch/patchwork-pkg-base",
3
- "version": "0.0.121",
3
+ "version": "0.0.122",
4
4
  "type": "module",
5
5
  "description": "Base packages for the Patchwork system",
6
6
  "repository": {
@@ -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": "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",
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": ";AAAA;AAAA,EACE,cAAAA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AACP;AAAA,EAGE;AAAA,EACA;AAAA,OACK;AACP,SAAS,kBAAkB;AAC3B,SAAS,2BAA2B;;;ACfpC,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,0CAA0C;AAAA,IACxC,gBAAgB;AAAA,IAChB,QAAQ;AAAA,EACV;AACF,CAAC;;;ADOM,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;AAUO,SAAS,OACd,OACA,KACkD;AAClD,QAAM,QAAQ,CAAC,MAAkB,MAAM,IAAI,YAAY,EAAE,MAAM,EAAE,EAAE;AACnE,aAAW,QAAQ,CAAC,GAAG,EAAE,GAAY;AACnC,aACM,OAA0B,WAAW,KAAK,EAAE,aAAa,KAAK,IAAI,GACtE,MACA,OAAO,KAAK,QACZ;AACA,YAAM,SAAS,KAAK,QAAQ,SAAS,UAAU,KAAK,QAAQ,SAAS;AACrE,UAAI,KAAK,SAAS,SAAS,CAAC;AAC1B,eAAO,EAAE,MAAM,KAAK,MAAM,IAAI,KAAK,IAAI,KAAK,MAAM,IAAI,EAAE;AAC1D,UAAI,KAAK,SAAS,UAAU,KAAK,SAAS,YAAY;AACpD,cAAM,MAAM,KAAK,SAAS,KAAK;AAC/B,eAAO,MAAM,EAAE,MAAM,KAAK,MAAM,IAAI,KAAK,IAAI,KAAK,MAAM,GAAG,EAAE,IAAI;AAAA,MACnE;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAEO,IAAM,QAAQ,CAAC,OAAoB,QACxC,OAAO,OAAO,GAAG,GAAG,OAAO;AAO7B,SAAS,iBAAiB,MAAkB,GAAW,GAAW;AAChE,QAAM,MAAM,KAAK,mBAAmB,EAAE,GAAG,EAAE,CAAC;AAC5C,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,KAAK,KAAK,YAAY,IAAI,KAAK,IAAI,KAAK;AAC9C,MACE,CAAC,MACD,IAAI,GAAG,OACP,IAAI,GAAG,UACP,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,KAAK,wBAAwB;AAErD,WAAO;AACT,SAAO,OAAO,KAAK,OAAO,IAAI,GAAG;AACnC;AAEA,IAAM,QACJ,OAAO,cAAc,eACrB,uBAAuB,KAAK,UAAU,YAAY,UAAU,SAAS;AACvE,IAAM,UAAU,CAAC,MACf,QAAQ,EAAE,UAAU,EAAE;AAGxB,IAAM,WAAW,YAAY,OAA4C;AACzE,IAAM,YAAY,WAAW,KAAK,EAAE,OAAO,mBAAmB,CAAC;AAE/D,IAAM,aAAa,WAAW,OAAsB;AAAA,EAClD,QAAQ,MAAM,WAAW;AAAA,EACzB,OAAO,MAAM,IAAI;AACf,WAAO,KAAK,IAAI,GAAG,OAAO;AAC1B,eAAW,KAAK,GAAG,SAAS;AAC1B,UAAI,EAAE,GAAG,QAAQ,GAAG;AAClB,eAAO,EAAE,QACL,WAAW,IAAI,UAAU,MAAM,EAAE,MAAM,MAAM,EAAE,MAAM,EAAE,CAAC,IACxD,WAAW;AAAA,MACjB;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA,EACA,SAAS,CAAC,MAAMC,YAAW,YAAY,KAAK,CAAC;AAC/C,CAAC;AAOD,IAAM,WAAW,WAAW;AAAA,EAC1B,MAAM;AAAA,IAIJ,YAAqB,MAAkB;AAAlB;AAAA,IAAmB;AAAA,IAHxC,UAA2C;AAAA,IAC3C,MAAM;AAAA,IAIN,UAAU;AACR,YAAM,EAAE,KAAK,IAAI;AACjB,YAAM,OACJ,KAAK,OAAO,KAAK,UACb,iBAAiB,MAAM,KAAK,QAAQ,GAAG,KAAK,QAAQ,CAAC,IACrD;AACN,UAAI,UAA+C;AACnD,WAAK,MAAM,MAAM,UAAU,EAAE,QAAQ,GAAG,KAAK,MAAM,IAAI,QAAQ,CAAC,MAAM,OAAO;AAC3E,kBAAU,EAAE,MAAM,GAAG;AAAA,MACvB,CAAC;AACD,YAAM,OAAO,CAAC,GAAmB,MAC/B,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,GAAG;AACtC,YAAM,OAAO,QAAQ,EAAE,MAAM,KAAK,MAAM,IAAI,KAAK,GAAG;AACpD,UAAI,CAAC,KAAK,SAAS,IAAI,EAAG,MAAK,SAAS,EAAE,SAAS,SAAS,GAAG,IAAI,EAAE,CAAC;AAAA,IACxE;AAAA,EACF;AAAA,EACA;AAAA,IACE,eAAe;AAAA,MACb,UAAU,OAAO,MAAM;AACrB,YAAI,MAAM,WAAW,KAAK,CAAC,QAAQ,KAAK,EAAG,QAAO;AAClD,cAAM,OAAO,iBAAiB,MAAM,MAAM,SAAS,MAAM,OAAO;AAChE,YAAI,CAAC,KAAM,QAAO;AAClB,cAAM,eAAe;AACrB,gBAAQ,KAAK,KAAK,KAAK,GAAG;AAC1B,eAAO;AAAA,MACT;AAAA,MACA,UAAU,OAAO;AACf,aAAK,UAAU,EAAE,GAAG,MAAM,SAAS,GAAG,MAAM,QAAQ;AACpD,aAAK,MAAM,QAAQ,KAAK;AACxB,aAAK,QAAQ;AAAA,MACf;AAAA,MACA,aAAa;AACX,aAAK,UAAU;AACf,aAAK,QAAQ;AAAA,MACf;AAAA,MACA,QAAQ,OAAO;AACb,aAAK,MAAM,QAAQ,KAAK;AACxB,aAAK,QAAQ;AAAA,MACf;AAAA,MACA,MAAM,OAAO;AACX,aAAK,MAAM,QAAQ,KAAK;AACxB,aAAK,QAAQ;AAAA,MACf;AAAA,MACA,OAAO;AACL,aAAK,MAAM;AACX,aAAK,QAAQ;AAAA,MACf;AAAA,IACF;AAAA,EACF;AACF;AAEO,SAAS,gBAAgB;AAC9B,SAAO,CAAC,YAAY,YAAY,UAAU,SAAS;AACrD;",
6
6
  "names": ["EditorView", "EditorView"]
7
7
  }
@@ -1 +1 @@
1
- automerge:2pRxD5g3CJepuE34h5FpNRgUEaj7
1
+ automerge:3nJh3fTdVVTn5bT5BrYYdUq4hRYF