@inkandswitch/patchwork-pkg-base 0.0.117 → 0.0.119
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.
- package/module-settings-url +1 -1
- package/package.json +1 -1
- package/packages/codemirror-markdown/dist/extensions/embed.js +79 -1
- package/packages/codemirror-markdown/dist/extensions/embed.js.map +2 -2
- package/packages/codemirror-markdown/dist/extensions/markdown.js +69 -25
- package/packages/codemirror-markdown/dist/extensions/markdown.js.map +4 -4
- package/plugin-registry-url +1 -1
package/module-settings-url
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
automerge:
|
|
1
|
+
automerge:2RX2vv2kin4WYv8B6Tf6r75tcMo9
|
package/package.json
CHANGED
|
@@ -26,6 +26,16 @@ var embedTheme = EditorView.baseTheme({
|
|
|
26
26
|
"&dark .cm-embed": {
|
|
27
27
|
borderColor: "#333"
|
|
28
28
|
},
|
|
29
|
+
// Images are just images: no border, natural size, never wider than the text.
|
|
30
|
+
"&light .cm-embed.cm-embed-image, &dark .cm-embed.cm-embed-image": {
|
|
31
|
+
border: "none",
|
|
32
|
+
borderRadius: "0"
|
|
33
|
+
},
|
|
34
|
+
".cm-embed-image > img": {
|
|
35
|
+
display: "block",
|
|
36
|
+
maxWidth: "100%",
|
|
37
|
+
height: "auto"
|
|
38
|
+
},
|
|
29
39
|
".cm-embed > patchwork-view": {
|
|
30
40
|
display: "block",
|
|
31
41
|
height: "500px",
|
|
@@ -51,6 +61,23 @@ var EmbedWidget = class extends WidgetType {
|
|
|
51
61
|
toDOM(view) {
|
|
52
62
|
const container = document.createElement("div");
|
|
53
63
|
container.className = "cm-embed";
|
|
64
|
+
if (this.toolId) {
|
|
65
|
+
this.mountFrame(view, container);
|
|
66
|
+
} else {
|
|
67
|
+
const cleanup = mountImageOrFrame(
|
|
68
|
+
this.docId,
|
|
69
|
+
container,
|
|
70
|
+
() => this.mountFrame(view, container)
|
|
71
|
+
);
|
|
72
|
+
cleanups.set(container, cleanup);
|
|
73
|
+
}
|
|
74
|
+
return container;
|
|
75
|
+
}
|
|
76
|
+
destroy(dom) {
|
|
77
|
+
cleanups.get(dom)?.();
|
|
78
|
+
cleanups.delete(dom);
|
|
79
|
+
}
|
|
80
|
+
mountFrame(view, container) {
|
|
54
81
|
const patchworkView = document.createElement("patchwork-view");
|
|
55
82
|
patchworkView.setAttribute("doc-url", `automerge:${this.docId}`);
|
|
56
83
|
patchworkView.setAttribute("tool-id", "embed");
|
|
@@ -64,7 +91,6 @@ var EmbedWidget = class extends WidgetType {
|
|
|
64
91
|
if (toolId) this.setTool(view, container, toolId);
|
|
65
92
|
});
|
|
66
93
|
container.appendChild(patchworkView);
|
|
67
|
-
return container;
|
|
68
94
|
}
|
|
69
95
|
setTool(view, container, toolId) {
|
|
70
96
|
const from = view.posAtDOM(container);
|
|
@@ -78,6 +104,58 @@ var EmbedWidget = class extends WidgetType {
|
|
|
78
104
|
return true;
|
|
79
105
|
}
|
|
80
106
|
};
|
|
107
|
+
var cleanups = /* @__PURE__ */ new WeakMap();
|
|
108
|
+
var IMAGE_EXTENSIONS = ["svg", "png", "jpg", "jpeg", "gif", "webp", "bmp"];
|
|
109
|
+
function isImageDoc(doc) {
|
|
110
|
+
if (doc?.["@patchwork"]?.type !== "file") return false;
|
|
111
|
+
if (!(doc.content instanceof Uint8Array)) return false;
|
|
112
|
+
return !!doc.mimeType?.startsWith("image/") || IMAGE_EXTENSIONS.includes(doc.extension?.toLowerCase() ?? "");
|
|
113
|
+
}
|
|
114
|
+
function mountImageOrFrame(docId, container, mountFrame) {
|
|
115
|
+
const repo = window.repo;
|
|
116
|
+
if (!repo?.find) {
|
|
117
|
+
mountFrame();
|
|
118
|
+
return () => {
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
let disposed = false;
|
|
122
|
+
let objectUrl;
|
|
123
|
+
let handle;
|
|
124
|
+
let img;
|
|
125
|
+
const render = () => {
|
|
126
|
+
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 ?? "";
|
|
134
|
+
};
|
|
135
|
+
Promise.resolve(repo.find(`automerge:${docId}`)).then(
|
|
136
|
+
(h) => {
|
|
137
|
+
if (disposed) return;
|
|
138
|
+
handle = h;
|
|
139
|
+
if (!isImageDoc(handle.doc())) {
|
|
140
|
+
mountFrame();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
container.classList.add("cm-embed-image");
|
|
144
|
+
img = document.createElement("img");
|
|
145
|
+
container.appendChild(img);
|
|
146
|
+
render();
|
|
147
|
+
handle.on("change", render);
|
|
148
|
+
},
|
|
149
|
+
() => {
|
|
150
|
+
if (!disposed) mountFrame();
|
|
151
|
+
}
|
|
152
|
+
);
|
|
153
|
+
return () => {
|
|
154
|
+
disposed = true;
|
|
155
|
+
handle?.off?.("change", render);
|
|
156
|
+
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
|
157
|
+
};
|
|
158
|
+
}
|
|
81
159
|
var EMBED_PATTERN = /\[patchwork:([^/\]]+)(?:\/([^\]]+))?\]/g;
|
|
82
160
|
function getEmbedLinks(view) {
|
|
83
161
|
const widgets = [];
|
|
@@ -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 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 return container;\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\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 \".cm-embed > patchwork-view\": {\n display: \"block\",\n height: \"500px\",\n width: \"100%\",\n },\n});\n"],
|
|
5
|
-
"mappings": ";AAAA;AAAA,EACE,cAAAA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,OAEK;AAEP;AAAA,EAGE;AAAA,EACA;AAAA,OACK;;;ACfP,SAAS,kBAAkB;AAGpB,IAAM,aAAa,WAAW,UAAU;AAAA,EAC7C,aAAa;AAAA,IACX,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,cAAc;AAAA,IACd,UAAU;AAAA,EACZ;AAAA,EACA,oBAAoB;AAAA,IAClB,aAAa;AAAA,EACf;AAAA,EACA,mBAAmB;AAAA,IACjB,aAAa;AAAA,EACf;AAAA,EACA,8BAA8B;AAAA,IAC5B,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,OAAO;AAAA,EACT;AACF,CAAC;;;
|
|
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": ";AAAA;AAAA,EACE,cAAAA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,OAEK;AAEP;AAAA,EAGE;AAAA,EACA;AAAA,OACK;;;ACfP,SAAS,kBAAkB;AAGpB,IAAM,aAAa,WAAW,UAAU;AAAA,EAC7C,aAAa;AAAA,IACX,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,cAAc;AAAA,IACd,UAAU;AAAA,EACZ;AAAA,EACA,oBAAoB;AAAA,IAClB,aAAa;AAAA,EACf;AAAA,EACA,mBAAmB;AAAA,IACjB,aAAa;AAAA,EACf;AAAA;AAAA,EAEA,mEAAmE;AAAA,IACjE,QAAQ;AAAA,IACR,cAAc;AAAA,EAChB;AAAA,EACA,yBAAyB;AAAA,IACvB,SAAS;AAAA,IACT,UAAU;AAAA,IACV,QAAQ;AAAA,EACV;AAAA,EACA,8BAA8B;AAAA,IAC5B,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,OAAO;AAAA,EACT;AACF,CAAC;;;ADND,IAAM,cAAN,cAA0B,WAAW;AAAA,EAC1B;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EAET,YAAY,OAAmB,QAAuB,WAAmB;AACvE,UAAM;AACN,SAAK,QAAQ;AACb,SAAK,SAAS;AACd,SAAK,YAAY;AAAA,EACnB;AAAA,EAEA,GAAG,OAAoB;AACrB,WAAO,MAAM,UAAU,KAAK,SAAS,MAAM,WAAW,KAAK;AAAA,EAC7D;AAAA,EAEA,MAAM,MAAkB;AACtB,UAAM,YAAY,SAAS,cAAc,KAAK;AAC9C,cAAU,YAAY;AAItB,QAAI,KAAK,QAAQ;AACf,WAAK,WAAW,MAAM,SAAS;AAAA,IACjC,OAAO;AACL,YAAM,UAAU;AAAA,QAAkB,KAAK;AAAA,QAAO;AAAA,QAAW,MACvD,KAAK,WAAW,MAAM,SAAS;AAAA,MACjC;AACA,eAAS,IAAI,WAAW,OAAO;AAAA,IACjC;AACA,WAAO;AAAA,EACT;AAAA,EAEA,QAAQ,KAAkB;AACxB,aAAS,IAAI,GAAG,IAAI;AACpB,aAAS,OAAO,GAAG;AAAA,EACrB;AAAA,EAEQ,WAAW,MAAkB,WAA8B;AACjE,UAAM,gBAAgB,SAAS,cAAc,gBAAgB;AAG7D,kBAAc,aAAa,WAAW,aAAa,KAAK,KAAK,EAAE;AAC/D,kBAAc,aAAa,WAAW,OAAO;AAC7C,QAAI,KAAK,OAAQ,eAAc,aAAa,iBAAiB,KAAK,MAAM;AAGxE,kBAAc,MAAM,UAAU;AAC9B,kBAAc,MAAM,SAAS;AAC7B,kBAAc,MAAM,QAAQ;AAI5B,cAAU,iBAAiB,gCAAgC,CAAC,MAAM;AAChE,QAAE,gBAAgB;AAClB,YAAM,SAAU,EAAuC,QAAQ;AAC/D,UAAI,OAAQ,MAAK,QAAQ,MAAM,WAAW,MAAM;AAAA,IAClD,CAAC;AAED,cAAU,YAAY,aAAa;AAAA,EACrC;AAAA,EAEQ,QACN,MACA,WACA,QACM;AACN,UAAM,OAAO,KAAK,SAAS,SAAS;AACpC,UAAM,KAAK,OAAO,KAAK,UAAU;AAEjC,QAAI,KAAK,MAAM,IAAI,YAAY,MAAM,EAAE,MAAM,KAAK,UAAW;AAC7D,SAAK,SAAS;AAAA,MACZ,SAAS,EAAE,MAAM,IAAI,QAAQ,YAAY,EAAE,OAAO,KAAK,OAAO,OAAO,CAAC,EAAE;AAAA,IAC1E,CAAC;AAAA,EACH;AAAA,EAEA,cAAc;AAEZ,WAAO;AAAA,EACT;AACF;AAEA,IAAM,WAAW,oBAAI,QAAiC;AAEtD,IAAM,mBAAmB,CAAC,OAAO,OAAO,OAAO,QAAQ,OAAO,QAAQ,KAAK;AAU3E,SAAS,WAAW,KAAmC;AACrD,MAAI,MAAM,YAAY,GAAG,SAAS,OAAQ,QAAO;AACjD,MAAI,EAAE,IAAI,mBAAmB,YAAa,QAAO;AACjD,SACE,CAAC,CAAC,IAAI,UAAU,WAAW,QAAQ,KACnC,iBAAiB,SAAS,IAAI,WAAW,YAAY,KAAK,EAAE;AAEhE;AAUA,SAAS,kBACP,OACA,WACA,YACY;AACZ,QAAM,OAAQ,OAAqC;AACnD,MAAI,CAAC,MAAM,MAAM;AACf,eAAW;AACX,WAAO,MAAM;AAAA,IAAC;AAAA,EAChB;AAEA,MAAI,WAAW;AACf,MAAI;AACJ,MAAI;AACJ,MAAI;AAEJ,QAAM,SAAS,MAAM;AACnB,UAAM,MAAM,OAAO,IAAI;AACvB,QAAI,CAAC,WAAW,GAAG,EAAG;AACtB,QAAI,UAAW,KAAI,gBAAgB,SAAS;AAC5C,gBAAY,IAAI;AAAA,MACd,IAAI,KAAK,CAAC,IAAK,OAAmB,GAAG,EAAE,MAAM,IAAK,YAAY,GAAG,CAAC;AAAA,IACpE;AACA,QAAK,MAAM;AACX,QAAK,MAAM,IAAK,QAAQ;AAAA,EAC1B;AAEA,UAAQ,QAAQ,KAAK,KAAK,aAAa,KAAK,EAAE,CAAC,EAAE;AAAA,IAC/C,CAAC,MAAM;AACL,UAAI,SAAU;AACd,eAAS;AACT,UAAI,CAAC,WAAW,OAAO,IAAI,CAAC,GAAG;AAC7B,mBAAW;AACX;AAAA,MACF;AACA,gBAAU,UAAU,IAAI,gBAAgB;AACxC,YAAM,SAAS,cAAc,KAAK;AAClC,gBAAU,YAAY,GAAG;AACzB,aAAO;AACP,aAAO,GAAG,UAAU,MAAM;AAAA,IAC5B;AAAA,IACA,MAAM;AACJ,UAAI,CAAC,SAAU,YAAW;AAAA,IAC5B;AAAA,EACF;AAEA,SAAO,MAAM;AACX,eAAW;AACX,YAAQ,MAAM,UAAU,MAAM;AAC9B,QAAI,UAAW,KAAI,gBAAgB,SAAS;AAAA,EAC9C;AACF;AAOA,IAAM,gBAAgB;AAEtB,SAAS,cAAc,MAAkB;AACvC,QAAM,UAA+B,CAAC;AACtC,QAAM,EAAE,MAAM,IAAI;AAIlB,aAAW,EAAE,MAAM,GAAG,KAAK,KAAK,eAAe;AAC7C,UAAM,OAAO,MAAM,IAAI,YAAY,MAAM,EAAE;AAC3C,kBAAc,YAAY;AAC1B,QAAI;AACJ,YAAQ,IAAI,cAAc,KAAK,IAAI,OAAO,MAAM;AAC9C,YAAM,YAAY,OAAO,EAAE;AAC3B,YAAM,UAAU,YAAY,EAAE,CAAC,EAAE;AACjC,YAAM,CAAC,WAAW,OAAO,MAAM,IAAI;AAEnC,UAAI,CAAC,kBAAkB,KAAK,EAAG;AAE/B,YAAM,QAAQ,WAAW,QAAQ;AAAA,QAC/B,QAAQ,IAAI,YAAY,OAAqB,UAAU,MAAM,SAAS;AAAA,MACxE,CAAC;AACD,cAAQ,KAAK,MAAM,MAAM,WAAW,OAAO,CAAC;AAAA,IAC9C;AAAA,EACF;AAIA,SAAO,WAAW,IAAI,SAAS,IAAI;AACrC;AAGA,IAAM,gBAAgB;AACtB,IAAM,iBAAiB;AAKvB,SAAS,WAAW,KAAgC;AAClD,QAAM,UAAU,IAAI,KAAK;AACzB,MAAI,CAAC,QAAS,QAAO;AAErB,QAAM,KAAK,QAAQ,MAAM,0BAA0B;AACnD,MAAI,MAAM,kBAAkB,GAAG,CAAC,CAAC,EAAG,QAAO,GAAG,CAAC;AAE/C,QAAM,MAAM,QAAQ,MAAM,yBAAyB;AACnD,MAAI,OAAO,kBAAkB,IAAI,CAAC,CAAC,EAAG,QAAO,IAAI,CAAC;AAClD,SAAO;AACT;AAOA,SAAS,eAAe,IAA4B;AAClD,QAAM,OAAiB,CAAC;AACxB,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,OAAO,CAAC,OAA0B,WAA0B;AAChE,QAAI,CAAC,SAAS,KAAK,IAAI,KAAK,EAAG;AAC/B,SAAK,IAAI,KAAK;AACd,SAAK,KAAK,EAAE,OAAO,OAAO,CAAC;AAAA,EAC7B;AAEA,QAAM,MAAM,GAAG,QAAQ,aAAa;AACpC,MAAI,KAAK;AACP,QAAI;AACF,YAAM,SAAS,KAAK,MAAM,GAAG;AAG7B,iBAAW,QAAQ,QAAQ,SAAS,CAAC,GAAG;AACtC,YAAI,MAAM,IAAK,MAAK,WAAW,KAAK,GAAG,GAAG,KAAK,UAAU,IAAI;AAAA,MAC/D;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AACA,MAAI,KAAK,SAAS,EAAG,QAAO;AAE5B,QAAM,OAAO,GAAG,QAAQ,cAAc;AACtC,MAAI,MAAM;AACR,QAAI;AACF,YAAM,SAAkB,KAAK,MAAM,IAAI;AACvC,UAAI,MAAM,QAAQ,MAAM,GAAG;AACzB,mBAAW,KAAK,OAAQ,MAAK,WAAW,OAAO,CAAC,CAAC,GAAG,IAAI;AAAA,MAC1D;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AACA,MAAI,KAAK,SAAS,EAAG,QAAO;AAE5B,QAAM,OAAO,GAAG,QAAQ,eAAe,KAAK,GAAG,QAAQ,YAAY;AACnE,MAAI,MAAM;AACR,eAAW,QAAQ,KAAK,MAAM,OAAO,GAAG;AACtC,UAAI,KAAK,WAAW,GAAG,EAAG;AAC1B,WAAK,WAAW,IAAI,GAAG,IAAI;AAAA,IAC7B;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,YAAY,EAAE,OAAO,OAAO,GAAmB;AACtD,SAAO,SAAS,cAAc,KAAK,IAAI,MAAM,MAAM,cAAc,KAAK;AACxE;AAQA,eAAe,eAAe,OAA0C;AACtE,QAAM,OAAQ,OAAqC;AACnD,MAAI,CAAC,MAAM;AACT,YAAQ,KAAK,4DAA4D;AACzE,WAAO,CAAC;AAAA,EACV;AACA,QAAM,OAAiB,CAAC;AACxB,aAAW,QAAQ,OAAO;AACxB,QAAI;AACF,YAAM,WAAW,KAAK,QAAQ;AAC9B,YAAM,SACJ,SAAS,WAAW,OAAO,KAAK,aAAa;AAC/C,YAAM,UAAU,SACZ,MAAM,KAAK,KAAK,IAChB,IAAI,WAAW,MAAM,KAAK,YAAY,CAAC;AAC3C,YAAM,QAAQ,KAAK,KAAK,MAAM,GAAG;AACjC,YAAM,YAAY,MAAM,SAAS,IAAI,MAAM,IAAI,IAAK;AACpD,YAAM,SAAS,KAAK,OAAO;AAC3B,aAAO,OAAO,CAAC,MAAW;AACxB,UAAE,YAAY,IAAI,EAAE,MAAM,OAAO;AACjC,UAAE,UAAU;AACZ,UAAE,WAAW;AACb,UAAE,YAAY;AACd,UAAE,OAAO,KAAK;AAAA,MAChB,CAAC;AACD,YAAM,EAAE,WAAW,IAAI,kBAAkB,OAAO,GAAmB;AACnE,UAAI,kBAAkB,UAAU,GAAG;AACjC,aAAK,KAAK,EAAE,OAAO,YAA0B,QAAQ,KAAK,CAAC;AAAA,MAC7D;AAAA,IACF,SAAS,KAAK;AACZ,cAAQ;AAAA,QACN;AAAA,QACA,KAAK;AAAA,QACL;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAGA,SAAS,WACP,MACA,MACA,MACA,KAAK,MACC;AACN,MAAI,KAAK,WAAW,EAAG;AAEvB,QAAM,MAAM,KAAK,MAAM,IAAI;AAC3B,SAAO,KAAK,IAAI,MAAM,GAAG;AACzB,OAAK,KAAK,IAAI,IAAI,GAAG;AACrB,MAAI,OAAO,KAAK,IAAI,WAAW,EAAE,KAAK,MAAM;AAE5C,QAAM,EAAE,IAAI,IAAI,KAAK;AACrB,QAAM,QAAQ,IAAI,OAAO,IAAI;AAC7B,QAAM,MAAM,IAAI,OAAO,EAAE;AACzB,MAAI,KAAK,KAAK,MAAM,IAAI,KAAK,OAAO,MAAM,KAAM,QAAO,OAAO;AAC9D,MAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,IAAI,GAAI,QAAO,OAAO;AACtD,OAAK,SAAS;AAAA,IACZ,SAAS,EAAE,MAAM,IAAI,QAAQ,KAAK;AAAA,IAClC,WAAW,EAAE,QAAQ,OAAO,KAAK,OAAO;AAAA,EAC1C,CAAC;AACH;AAUA,SAAS,oBAAoB;AAI3B,QAAM,gBAAgB,CAAC,OACrB,CAAC,CAAC,OACD,GAAG,MAAM,SAAS,OAAO,KACxB,GAAG,MAAM,SAAS,aAAa,KAC/B,GAAG,MAAM,SAAS,cAAc;AAEpC,SAAOC,YAAW,iBAAiB;AAAA,IACjC,SAAS,OAAO;AACd,YAAM,KAAK,MAAM;AACjB,UAAI,CAAC,cAAc,EAAE,EAAG,QAAO;AAC/B,YAAM,eAAe;AAGrB,UAAI,GAAI,IAAG,aAAa,GAAG,MAAM,SAAS,OAAO,IAAI,SAAS;AAC9D,aAAO;AAAA,IACT;AAAA,IACA,KAAK,OAAO,MAAM;AAChB,YAAM,KAAK,MAAM;AACjB,UAAI,CAAC,GAAI,QAAO;AAChB,YAAM,MAAM,KAAK,YAAY,EAAE,GAAG,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC;AACnE,UAAI,OAAO,KAAM,QAAO;AAGxB,UAAI,GAAG,SAAS,GAAG,MAAM,SAAS,GAAG;AACnC,cAAM,eAAe;AACrB,aAAK,eAAe,MAAM,KAAK,GAAG,KAAK,CAAC,EAAE;AAAA,UAAK,CAACC,UAC9C,WAAW,MAAM,KAAKA,KAAI;AAAA,QAC5B;AACA,eAAO;AAAA,MACT;AAKA,YAAM,OAAO,eAAe,EAAE;AAC9B,UAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,YAAM,eAAe;AACrB,iBAAW,MAAM,KAAK,IAAI;AAC1B,aAAO;AAAA,IACT;AAAA,IACA,MAAM,OAAO,MAAM;AAKjB,YAAM,KAAK,MAAM;AACjB,UAAI,CAAC,MAAM,GAAG,QAAQ,YAAY,EAAG,QAAO;AAC5C,YAAM,SAAS,MAAM,KAAK,GAAG,SAAS,CAAC,CAAC,EAAE;AAAA,QAAO,CAAC,MAChD,EAAE,KAAK,WAAW,QAAQ;AAAA,MAC5B;AACA,UAAI,OAAO,WAAW,EAAG,QAAO;AAChC,YAAM,eAAe;AACrB,YAAM,EAAE,MAAM,GAAG,IAAI,KAAK,MAAM,UAAU;AAC1C,WAAK,eAAe,MAAM,EAAE;AAAA,QAAK,CAAC,SAChC,WAAW,MAAM,MAAM,MAAM,EAAE;AAAA,MACjC;AACA,aAAO;AAAA,IACT;AAAA,EACF,CAAC;AACH;AAEA,IAAM,cAAc,WAAW;AAAA,EAC7B,MAAM;AAAA,IACJ;AAAA,IAEA,YAAY,MAAkB;AAC5B,WAAK,cAAc,cAAc,IAAI;AAAA,IACvC;AAAA,IAEA,OAAO,QAAoB;AAEzB,UAAI,OAAO,cAAc,OAAO,iBAAiB;AAC/C,aAAK,cAAc,cAAc,OAAO,IAAI;AAAA,MAC9C;AAAA,IACF;AAAA,EACF;AAAA,EACA;AAAA,IACE,aAAa,CAAC,MAAM,EAAE;AAAA;AAAA,IAEtB,SAAS,CAAC,WACRD,YAAW,aAAa;AAAA,MACtB,CAAC,SAAS,KAAK,OAAO,MAAM,GAAG,eAAe,WAAW;AAAA,IAC3D;AAAA,EACJ;AACF;AAEO,SAAS,gBAAgB;AAE9B,SAAO,CAAC,aAAa,YAAY,kBAAkB,GAAG,WAAW,CAAC;AACpE;",
|
|
6
6
|
"names": ["EditorView", "EditorView", "refs"]
|
|
7
7
|
}
|
|
@@ -2065,7 +2065,7 @@ var searchExtensions = [
|
|
|
2065
2065
|
];
|
|
2066
2066
|
|
|
2067
2067
|
// src/extensions/markdown.ts
|
|
2068
|
-
import { EditorView as
|
|
2068
|
+
import { EditorView as EditorView5, keymap as keymap2 } from "@codemirror/view";
|
|
2069
2069
|
|
|
2070
2070
|
// src/extensions/formatting.ts
|
|
2071
2071
|
import { EditorSelection as EditorSelection2 } from "@codemirror/state";
|
|
@@ -2220,6 +2220,48 @@ var taskCheckboxes = ViewPlugin2.fromClass(
|
|
|
2220
2220
|
{ decorations: (v) => v.decorations }
|
|
2221
2221
|
);
|
|
2222
2222
|
|
|
2223
|
+
// src/extensions/zoom.ts
|
|
2224
|
+
import { Prec as Prec2 } from "@codemirror/state";
|
|
2225
|
+
import { keymap } from "@codemirror/view";
|
|
2226
|
+
var MIN_FONT_SIZE = 6;
|
|
2227
|
+
var FALLBACK_FONT_SIZE = 16;
|
|
2228
|
+
var px = (value) => parseFloat(value) || FALLBACK_FONT_SIZE;
|
|
2229
|
+
function baseSize(view) {
|
|
2230
|
+
const { style } = view.dom;
|
|
2231
|
+
const zoomed2 = style.fontSize;
|
|
2232
|
+
style.fontSize = "";
|
|
2233
|
+
const base = px(getComputedStyle(view.dom).fontSize);
|
|
2234
|
+
style.fontSize = zoomed2;
|
|
2235
|
+
return base;
|
|
2236
|
+
}
|
|
2237
|
+
function step(view, by) {
|
|
2238
|
+
const { style } = view.dom;
|
|
2239
|
+
if (by === 0) {
|
|
2240
|
+
style.fontSize = "";
|
|
2241
|
+
style.removeProperty("--markdown-zoom");
|
|
2242
|
+
} else {
|
|
2243
|
+
const current = px(getComputedStyle(view.dom).fontSize);
|
|
2244
|
+
const size = Math.max(MIN_FONT_SIZE, current + by);
|
|
2245
|
+
style.fontSize = `${size}px`;
|
|
2246
|
+
style.setProperty("--markdown-zoom", String(size / baseSize(view)));
|
|
2247
|
+
}
|
|
2248
|
+
view.requestMeasure();
|
|
2249
|
+
return true;
|
|
2250
|
+
}
|
|
2251
|
+
var zoomIn = (view) => step(view, 1);
|
|
2252
|
+
var zoomOut = (view) => step(view, -1);
|
|
2253
|
+
var zoomReset = (view) => step(view, 0);
|
|
2254
|
+
var markdownZoom = Prec2.high(
|
|
2255
|
+
keymap.of([
|
|
2256
|
+
// `+` and `_` are the shifted keys, for keyboards that need Shift.
|
|
2257
|
+
{ key: "Mod-=", run: zoomIn, preventDefault: true },
|
|
2258
|
+
{ key: "Mod-+", run: zoomIn, preventDefault: true },
|
|
2259
|
+
{ key: "Mod--", run: zoomOut, preventDefault: true },
|
|
2260
|
+
{ key: "Mod-_", run: zoomOut, preventDefault: true },
|
|
2261
|
+
{ key: "Mod-0", run: zoomReset, preventDefault: true }
|
|
2262
|
+
])
|
|
2263
|
+
);
|
|
2264
|
+
|
|
2223
2265
|
// src/themes/markdown.ts
|
|
2224
2266
|
import {
|
|
2225
2267
|
HighlightStyle,
|
|
@@ -2229,11 +2271,12 @@ import {
|
|
|
2229
2271
|
import { RangeSetBuilder as RangeSetBuilder3 } from "@codemirror/state";
|
|
2230
2272
|
import {
|
|
2231
2273
|
Decoration as Decoration3,
|
|
2232
|
-
EditorView as
|
|
2274
|
+
EditorView as EditorView4,
|
|
2233
2275
|
ViewPlugin as ViewPlugin3,
|
|
2234
2276
|
WidgetType as WidgetType2
|
|
2235
2277
|
} from "@codemirror/view";
|
|
2236
2278
|
var bodyFont = (style) => style == "serif" ? 'var(--text-editor-family-serif, "Merriweather", serif)' : "var(--text-editor-family-sans)";
|
|
2279
|
+
var zoomed = (length) => `calc(${length} * var(--markdown-zoom, 1))`;
|
|
2237
2280
|
var markdownStyles = (style) => ({
|
|
2238
2281
|
"&": {},
|
|
2239
2282
|
"&.cm-editor.cm-focused": {
|
|
@@ -2258,15 +2301,15 @@ var markdownStyles = (style) => ({
|
|
|
2258
2301
|
// textAlign: "justify",
|
|
2259
2302
|
textWrap: "pretty",
|
|
2260
2303
|
fontFamily: bodyFont(style),
|
|
2261
|
-
lineHeight: "1.5rem",
|
|
2304
|
+
lineHeight: zoomed("1.5rem"),
|
|
2262
2305
|
color: "var(--text-editor-line)",
|
|
2263
2306
|
caretColor: "var(--text-editor-cursor-fill)",
|
|
2264
2307
|
background: "var(--text-editor-fill)",
|
|
2265
|
-
marginBlock: "2rem",
|
|
2308
|
+
marginBlock: zoomed("2rem"),
|
|
2266
2309
|
marginInline: "auto",
|
|
2267
2310
|
// lol i guess this is the same as box-sizing: content-box
|
|
2268
|
-
maxWidth:
|
|
2269
|
-
padding: "2rem",
|
|
2311
|
+
maxWidth: `calc(var(--max-text-line-width, 100ex) - ${zoomed("2rem")})`,
|
|
2312
|
+
padding: zoomed("2rem"),
|
|
2270
2313
|
boxShadow: "none",
|
|
2271
2314
|
marginBottom: "8em",
|
|
2272
2315
|
borderRadius: "8px"
|
|
@@ -2300,7 +2343,7 @@ var markdownStyles = (style) => ({
|
|
|
2300
2343
|
backgroundColor: "inherit"
|
|
2301
2344
|
},
|
|
2302
2345
|
".frontmatter, .frontmatter *": {
|
|
2303
|
-
fontSize: "14px",
|
|
2346
|
+
fontSize: zoomed("14px"),
|
|
2304
2347
|
fontFamily: "var(--text-editor-family-code)",
|
|
2305
2348
|
color: "var(--syntax-comment, #666)",
|
|
2306
2349
|
textDecoration: "none",
|
|
@@ -2352,33 +2395,33 @@ var markdownSyntaxHighlighting = (style) => HighlightStyle.define([
|
|
|
2352
2395
|
{
|
|
2353
2396
|
tag: tags.heading1,
|
|
2354
2397
|
...baseHeadingStyles,
|
|
2355
|
-
fontSize:
|
|
2356
|
-
lineHeight: "2rem",
|
|
2357
|
-
marginBottom: "1rem",
|
|
2358
|
-
marginTop: "2rem"
|
|
2398
|
+
fontSize: `var(--studio-heading-size-1, ${zoomed("1.5rem")})`,
|
|
2399
|
+
lineHeight: zoomed("2rem"),
|
|
2400
|
+
marginBottom: zoomed("1rem"),
|
|
2401
|
+
marginTop: zoomed("2rem")
|
|
2359
2402
|
},
|
|
2360
2403
|
{
|
|
2361
2404
|
tag: tags.heading2,
|
|
2362
2405
|
...baseHeadingStyles,
|
|
2363
|
-
fontSize:
|
|
2364
|
-
lineHeight: "2rem",
|
|
2365
|
-
marginBottom: "1rem",
|
|
2366
|
-
marginTop: "2rem"
|
|
2406
|
+
fontSize: `var(--studio-heading-size-2, ${zoomed("1.5rem")})`,
|
|
2407
|
+
lineHeight: zoomed("2rem"),
|
|
2408
|
+
marginBottom: zoomed("1rem"),
|
|
2409
|
+
marginTop: zoomed("2rem")
|
|
2367
2410
|
},
|
|
2368
2411
|
{
|
|
2369
2412
|
tag: tags.heading3,
|
|
2370
2413
|
...baseHeadingStyles,
|
|
2371
|
-
fontSize:
|
|
2372
|
-
lineHeight: "1.75rem",
|
|
2373
|
-
marginBottom: "1rem",
|
|
2374
|
-
marginTop: "2rem"
|
|
2414
|
+
fontSize: `var(--studio-heading-size-3, ${zoomed("1.25rem")})`,
|
|
2415
|
+
lineHeight: zoomed("1.75rem"),
|
|
2416
|
+
marginBottom: zoomed("1rem"),
|
|
2417
|
+
marginTop: zoomed("2rem")
|
|
2375
2418
|
},
|
|
2376
2419
|
{
|
|
2377
2420
|
tag: tags.heading4,
|
|
2378
2421
|
...baseHeadingStyles,
|
|
2379
|
-
fontSize:
|
|
2380
|
-
marginBottom: "1rem",
|
|
2381
|
-
marginTop: "2rem"
|
|
2422
|
+
fontSize: `var(--studio-heading-size-4, ${zoomed("1.1rem")})`,
|
|
2423
|
+
marginBottom: zoomed("1rem"),
|
|
2424
|
+
marginTop: zoomed("2rem")
|
|
2382
2425
|
},
|
|
2383
2426
|
{
|
|
2384
2427
|
tag: tags.processingInstruction,
|
|
@@ -2448,7 +2491,7 @@ var markdownSyntaxHighlighting = (style) => HighlightStyle.define([
|
|
|
2448
2491
|
{ tag: tags.monospace, ...baseCodeStyles }
|
|
2449
2492
|
]);
|
|
2450
2493
|
var theme = (style) => [
|
|
2451
|
-
|
|
2494
|
+
EditorView4.theme(markdownStyles(style)),
|
|
2452
2495
|
syntaxHighlighting(markdownSyntaxHighlighting(style)),
|
|
2453
2496
|
bullets,
|
|
2454
2497
|
codeblocks
|
|
@@ -2529,7 +2572,8 @@ function markdownExtensions() {
|
|
|
2529
2572
|
return [
|
|
2530
2573
|
...theme("sans"),
|
|
2531
2574
|
indentOnInput(),
|
|
2532
|
-
|
|
2575
|
+
markdownZoom,
|
|
2576
|
+
keymap2.of([
|
|
2533
2577
|
...markdownFormattingKeymap,
|
|
2534
2578
|
...defaultKeymap,
|
|
2535
2579
|
...searchKeymap,
|
|
@@ -2537,7 +2581,7 @@ function markdownExtensions() {
|
|
|
2537
2581
|
...completionKeymap,
|
|
2538
2582
|
indentWithTab
|
|
2539
2583
|
]),
|
|
2540
|
-
|
|
2584
|
+
EditorView5.lineWrapping,
|
|
2541
2585
|
// GFM base: task lists, tables, strikethrough, autolinks.
|
|
2542
2586
|
markdown({ base: markdownLanguage, codeLanguages: languages }),
|
|
2543
2587
|
taskCheckboxes,
|