@panphora/clayjs 1.13.1 → 1.15.0

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.
Files changed (146) hide show
  1. package/THIRD-PARTY-NOTICES.md +19 -0
  2. package/dist/clay.standalone.js +69385 -40992
  3. package/dist/server/checkpoint.cjs +1 -0
  4. package/dist/server/checkpoint.mjs +8 -0
  5. package/dist/server/core.cjs +72210 -0
  6. package/dist/server/rooms.cjs +1 -0
  7. package/dist/server/rooms.mjs +66 -0
  8. package/docs/guide.md +127 -99
  9. package/entries/clay.js +22 -0
  10. package/entries/sap.js +5 -5
  11. package/package.json +16 -2
  12. package/packed-contract.json +18 -3
  13. package/src/core/boot-phase.js +6 -0
  14. package/src/core/conflict-download.js +12 -3
  15. package/src/core/host-meta.js +75 -0
  16. package/src/core/persist.js +11 -0
  17. package/src/core/save-core.js +266 -31
  18. package/src/core/save.js +154 -36
  19. package/src/core/snapshot.js +51 -13
  20. package/src/core/style-comparisons.js +37 -0
  21. package/src/dom/insert-style-tag.js +11 -12
  22. package/src/lib/derived.js +116 -0
  23. package/src/lib/effect.js +130 -0
  24. package/src/lib/mutation.js +3 -1
  25. package/src/lib/style-adoption.js +9 -0
  26. package/src/lib/style.js +47 -0
  27. package/src/live/fast-lane.js +347 -60
  28. package/src/live/local-lane.js +32 -6
  29. package/src/live/ops.js +77 -0
  30. package/src/live/provenance.js +129 -0
  31. package/src/live/save-history.js +188 -0
  32. package/src/live/server-lane.js +9 -2
  33. package/src/live/store.js +194 -16
  34. package/src/loader-logic.js +16 -4
  35. package/src/loader.js +37 -1
  36. package/src/page/address.js +20 -0
  37. package/src/page/adopt-boot.js +189 -0
  38. package/src/page/adopt.js +240 -0
  39. package/src/page/ai-tool.js +188 -0
  40. package/src/page/announce.js +21 -0
  41. package/src/page/blocks.js +1170 -0
  42. package/src/page/blocksel.js +269 -0
  43. package/src/page/board-drag.js +226 -0
  44. package/src/page/board-panel.js +353 -0
  45. package/src/page/board-style.js +146 -0
  46. package/src/page/board-view.js +447 -0
  47. package/src/page/board.js +41 -0
  48. package/src/page/chart.js +220 -0
  49. package/src/page/checklist.js +143 -0
  50. package/src/page/chrome.js +587 -0
  51. package/src/page/csv-import.js +169 -0
  52. package/src/page/drag.js +202 -0
  53. package/src/page/drop.js +112 -0
  54. package/src/page/editor.js +386 -0
  55. package/src/page/highlight.js +268 -0
  56. package/src/page/inline.js +60 -1
  57. package/src/page/items.js +159 -0
  58. package/src/page/link.js +247 -0
  59. package/src/page/markdown.js +71 -0
  60. package/src/page/media.js +570 -0
  61. package/src/page/merge.js +130 -0
  62. package/src/page/model.js +71 -9
  63. package/src/page/placeholder.js +23 -0
  64. package/src/page/projection.js +647 -0
  65. package/src/page/recover.js +193 -0
  66. package/src/page/rich/cell.js +16 -0
  67. package/src/page/rich/chart.js +104 -0
  68. package/src/page/rich/csv.js +118 -0
  69. package/src/page/rich/page-tool.js +86 -0
  70. package/src/page/rich/tool-runtime.js +195 -0
  71. package/src/page/save-projection.js +53 -0
  72. package/src/page/save-states.js +85 -0
  73. package/src/page/schema.js +12 -5
  74. package/src/page/table-kinds.js +20 -0
  75. package/src/page/table-paste.js +57 -0
  76. package/src/page/table-select.js +244 -0
  77. package/src/page/table-style.js +25 -0
  78. package/src/page/table.js +650 -0
  79. package/src/page/themes/content.css +46 -0
  80. package/src/page/themes/light-1.css +269 -0
  81. package/src/page/themes/light-2.css +211 -0
  82. package/src/page/themes/light-3.css +130 -0
  83. package/src/page/themes/light-4.css +299 -0
  84. package/src/page/tool-data.js +25 -0
  85. package/src/page/tool-view.js +68 -0
  86. package/src/page/tools.js +133 -0
  87. package/src/page/view.js +136 -0
  88. package/src/page/ybind/author.js +25 -0
  89. package/src/page/ybind/binding.js +92 -0
  90. package/src/page/ybind/board-commands.js +170 -0
  91. package/src/page/ybind/board.js +89 -0
  92. package/src/page/ybind/bridge.js +382 -0
  93. package/src/page/ybind/conversion.js +26 -0
  94. package/src/page/ybind/core.js +7 -0
  95. package/src/page/ybind/decode.js +208 -0
  96. package/src/page/ybind/doc.js +58 -0
  97. package/src/page/ybind/external.js +190 -0
  98. package/src/page/ybind/group.js +124 -0
  99. package/src/page/ybind/index.js +12 -0
  100. package/src/page/ybind/layout.js +27 -0
  101. package/src/page/ybind/marks.js +18 -0
  102. package/src/page/ybind/migrate.js +152 -0
  103. package/src/page/ybind/moves.js +288 -0
  104. package/src/page/ybind/origins.js +257 -0
  105. package/src/page/ybind/parts.js +44 -0
  106. package/src/page/ybind/projection-map.js +92 -0
  107. package/src/page/ybind/provider.js +285 -0
  108. package/src/page/ybind/recovery.js +411 -0
  109. package/src/page/ybind/seed.js +27 -0
  110. package/src/page/ybind/table.js +166 -0
  111. package/src/page/ybind/tokens.js +93 -0
  112. package/src/page/ybind/walk.js +333 -0
  113. package/src/page/ybind/writer.js +307 -0
  114. package/src/plugins/derive.js +12 -0
  115. package/src/plugins/indicator.js +16 -6
  116. package/src/plugins/live.js +742 -65
  117. package/src/plugins/page.js +547 -0
  118. package/src/plugins/undo.js +62 -4
  119. package/src/server/checkpoint/index.js +218 -0
  120. package/src/server/rooms/index.js +59 -0
  121. package/src/server/rooms/live-data-ops.js +369 -0
  122. package/src/server/rooms/live-data-rooms.js +965 -0
  123. package/src/server/rooms/live-data-tags.js +49 -0
  124. package/src/server/rooms/live-page-history.js +41 -0
  125. package/src/server/rooms/live-provenance.js +142 -0
  126. package/src/server/rooms/page-admission.js +79 -0
  127. package/src/server/rooms/page-conversion.js +62 -0
  128. package/src/server/rooms/page-rebase.js +108 -0
  129. package/src/server/rooms/page-state.js +91 -0
  130. package/src/server/rooms/page-write-admission.js +63 -0
  131. package/src/server/rooms/page-write-guard.js +23 -0
  132. package/src/server/rooms/recent.js +18 -0
  133. package/src/server/rooms/spec-wire.js +17 -0
  134. package/src/standalone.js +22 -0
  135. package/src/sync/conflict-revert.js +2 -0
  136. package/src/sync/conflicts.js +2 -2
  137. package/src/sync/live-sync.js +36 -8
  138. package/src/sync/stream.js +62 -16
  139. package/src/ui/index.js +4 -0
  140. package/src/ui/toast.js +21 -5
  141. package/src/vendor/pierre/pierre.vendor.js +3 -0
  142. package/src/vendor/sap-core.vendor.js +9 -0
  143. package/src/vendor/sap-ownership.vendor.js +111 -0
  144. package/src/vendor/yjs.vendor.js +7 -0
  145. package/src/page/binding.js +0 -647
  146. package/src/page/replay.js +0 -578
@@ -0,0 +1,247 @@
1
+ // Link addresses the editor will store. #fragments, same-site paths, http(s),
2
+ // mailto and tel pass; a bare domain gets https:// and a bare address mailto:.
3
+ // Any other scheme (javascript:, data:, vbscript:, file:) is refused with null.
4
+ import { state, view as pmView } from "../vendor/prosemirror.vendor.js";
5
+ import { schema } from "./schema.js";
6
+ import { safeHref } from "./address.js";
7
+ import { asStep } from "./blocksel.js";
8
+
9
+ const { Plugin, PluginKey, TextSelection } = state;
10
+ const { Decoration, DecorationSet } = pmView;
11
+ const LINK = schema.marks.link;
12
+
13
+ export { safeHref, safeSrc } from "./address.js";
14
+ export const isUrlText = (text) => /^(https?:\/\/|mailto:|tel:|www\.)\S+$/i.test(text.trim()) && safeHref(text) !== null;
15
+
16
+ export function linkRange(doc, pos) {
17
+ const $p = doc.resolve(pos);
18
+ if (!$p.parent.inlineContent) return null;
19
+ const mark = LINK.isInSet(($p.nodeAfter ?? $p.nodeBefore)?.marks ?? []);
20
+ if (!mark) return null;
21
+ const kids = [];
22
+ $p.parent.forEach((n, off) => kids.push({ n, off }));
23
+ const i = kids.findIndex(({ n, off }) => off <= $p.parentOffset && $p.parentOffset <= off + n.nodeSize && mark.isInSet(n.marks));
24
+ if (i < 0) return null;
25
+ let a = i, b = i;
26
+ while (a > 0 && mark.isInSet(kids[a - 1].n.marks)) a--;
27
+ while (b < kids.length - 1 && mark.isInSet(kids[b + 1].n.marks)) b++;
28
+ const start = $p.start();
29
+ return { from: start + kids[a].off, to: start + kids[b].off + kids[b].n.nodeSize, href: mark.attrs.href };
30
+ }
31
+
32
+ export function setLinkTr(st, from, to, href) {
33
+ const tr = st.tr.removeMark(from, to, LINK);
34
+ if (href) tr.addMark(from, to, LINK.create({ href }));
35
+ return asStep(tr);
36
+ }
37
+
38
+ export const linkKey = new PluginKey("page-link");
39
+ const popovers = new WeakMap();
40
+
41
+ const svg = (body) => `<svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${body}</svg>`;
42
+ const ICON_OPEN = svg('<path d="M9 3h4v4M13 3 7.5 8.5M11 9.5V13H3V5h3.5"/>');
43
+ const ICON_REMOVE = svg('<path d="M4 4l8 8M12 4l-8 8"/>');
44
+
45
+ function el(doc, tag, cls, attrs = {}) {
46
+ const e = doc.createElement(tag);
47
+ if (cls) e.className = cls;
48
+ for (const [k, v] of Object.entries(attrs)) e.setAttribute(k, v);
49
+ return e;
50
+ }
51
+
52
+ function linkAt(st, from, to) {
53
+ let href = null;
54
+ st.doc.nodesBetween(from, to, (n) => { const m = LINK.isInSet(n.marks); if (m) href = m.attrs.href; });
55
+ return href;
56
+ }
57
+
58
+ class LinkPopover {
59
+ constructor(view) {
60
+ this.view = view;
61
+ this.shell = view.dom.parentElement;
62
+ this.doc = view.dom.ownerDocument;
63
+ this.mode = null;
64
+ this.range = null;
65
+ const doc = this.doc;
66
+ this.dom = el(doc, "div", "page-link", { role: "dialog", "aria-label": "Link", hidden: "" });
67
+ this.input = el(doc, "input", "page-link-input", { type: "url", "aria-label": "Link address", placeholder: "Paste or type a link", autocomplete: "off", spellcheck: "false" });
68
+ this.url = el(doc, "a", "page-link-url", { target: "_blank", rel: "noopener noreferrer" });
69
+ this.open = el(doc, "a", "page-link-btn", { "data-act": "open", target: "_blank", rel: "noopener noreferrer", "aria-label": "Open link in a new tab", title: "Open in a new tab" });
70
+ this.open.innerHTML = ICON_OPEN;
71
+ this.edit = el(doc, "button", "page-link-btn", { type: "button", "data-act": "edit" });
72
+ this.edit.textContent = "Edit";
73
+ this.remove = el(doc, "button", "page-link-btn", { type: "button", "data-act": "remove", "aria-label": "Remove link", title: "Remove link" });
74
+ this.remove.innerHTML = ICON_REMOVE;
75
+ this.dom.append(this.input, this.url, this.open, this.edit, this.remove);
76
+ this.shell.append(this.dom);
77
+ popovers.set(view, this);
78
+ this.dom.addEventListener("mousedown", (e) => { if (e.target !== this.input) e.preventDefault(); });
79
+ this.edit.addEventListener("click", () => this.openEdit());
80
+ this.remove.addEventListener("click", () => this.apply(""));
81
+ this.input.addEventListener("keydown", (e) => {
82
+ if (e.key === "Enter") { e.preventDefault(); this.apply(this.input.value); }
83
+ else if (e.key === "Escape") { e.preventDefault(); this.close(); }
84
+ });
85
+ this.onDocDown = (e) => { if (this.mode === "edit" && !this.dom.contains(e.target)) this.close(false); };
86
+ this.doc.addEventListener("mousedown", this.onDocDown, true);
87
+ }
88
+
89
+ openEdit() {
90
+ const st = this.view.state;
91
+ const sel = st.selection;
92
+ if (!(sel instanceof TextSelection) || !sel.$from.sameParent(sel.$to) || !sel.$from.parent.inlineContent || sel.$from.parent.type.spec.code) return false;
93
+ const around = sel.empty ? linkRange(st.doc, sel.head) : null;
94
+ this.range = around ?? { from: sel.from, to: sel.to, href: sel.empty ? null : linkAt(st, sel.from, sel.to) };
95
+ this.mode = "edit";
96
+ this.view.dispatch(st.tr.setMeta(linkKey, { from: this.range.from, to: this.range.to }));
97
+ this.render();
98
+ this.input.value = this.range.href ?? "";
99
+ this.input.removeAttribute("aria-invalid");
100
+ this.input.focus();
101
+ this.input.select();
102
+ return true;
103
+ }
104
+
105
+ apply(raw) {
106
+ const text = String(raw).trim();
107
+ const href = text ? safeHref(text) : null;
108
+ if (text && !href) { this.input.setAttribute("aria-invalid", "true"); return; }
109
+ const st = this.view.state;
110
+ const r = linkKey.getState(st) ?? (this.mode === "view" ? this.range : null);
111
+ if (!r) return this.close();
112
+ let tr;
113
+ if (r.from === r.to) {
114
+ if (!href) return this.close();
115
+ tr = st.tr.insert(r.from, schema.text(text, [LINK.create({ href })]));
116
+ tr.setSelection(TextSelection.create(tr.doc, r.from + text.length));
117
+ } else {
118
+ tr = setLinkTr(st, r.from, r.to, href);
119
+ tr.setSelection(TextSelection.create(tr.doc, r.to));
120
+ }
121
+ this.view.dispatch(asStep(tr).setMeta(linkKey, null));
122
+ this.close();
123
+ }
124
+
125
+ close(refocus = true) {
126
+ this.mode = null;
127
+ this.range = null;
128
+ if (linkKey.getState(this.view.state)) this.view.dispatch(this.view.state.tr.setMeta(linkKey, null));
129
+ this.dom.hidden = true;
130
+ this.dom.dataset.mode = "";
131
+ if (refocus) this.view.focus();
132
+ this.update(this.view);
133
+ }
134
+
135
+ render() {
136
+ const editing = this.mode === "edit";
137
+ const href = safeHref(this.range?.href);
138
+ this.dom.dataset.mode = this.mode ?? "";
139
+ this.input.hidden = !editing;
140
+ this.url.hidden = editing;
141
+ this.edit.hidden = editing;
142
+ this.open.hidden = !href;
143
+ this.remove.hidden = !this.range?.href;
144
+ if (href) { this.open.href = href; this.url.href = href; this.url.textContent = href.replace(/^https?:\/\//, ""); }
145
+ else { this.open.removeAttribute("href"); this.url.removeAttribute("href"); this.url.textContent = ""; }
146
+ this.dom.hidden = false;
147
+ this.place();
148
+ }
149
+
150
+ place() {
151
+ const r = this.range;
152
+ if (!r) return;
153
+ const shellBox = this.shell.getBoundingClientRect();
154
+ const a = this.view.coordsAtPos(r.from), b = this.view.coordsAtPos(r.to, -1);
155
+ this.dom.style.setProperty("--page-pop-x", `${Math.min(a.left, b.left) - shellBox.left}px`);
156
+ this.dom.style.setProperty("--page-pop-y", `${Math.max(a.bottom, b.bottom) - shellBox.top + 6}px`);
157
+ }
158
+
159
+ update(view) {
160
+ this.view = view;
161
+ if (this.mode === "edit") {
162
+ const range = linkKey.getState(view.state);
163
+ if (range) { this.range = { ...this.range, ...range }; return this.place(); }
164
+ this.mode = null;
165
+ this.range = null;
166
+ this.dom.hidden = true;
167
+ this.dom.dataset.mode = "";
168
+ return;
169
+ }
170
+ const sel = view.state.selection;
171
+ const r = sel instanceof TextSelection && sel.empty && view.hasFocus() ? linkRange(view.state.doc, sel.head) : null;
172
+ if (!r || sel.head === r.from || sel.head === r.to) {
173
+ this.mode = null;
174
+ this.range = null;
175
+ this.dom.hidden = true;
176
+ this.dom.dataset.mode = "";
177
+ return;
178
+ }
179
+ this.mode = "view";
180
+ this.range = r;
181
+ this.render();
182
+ }
183
+
184
+ destroy() {
185
+ this.doc.removeEventListener("mousedown", this.onDocDown, true);
186
+ this.dom.remove();
187
+ popovers.delete(this.view);
188
+ }
189
+ }
190
+
191
+ export const openLinkEditor = (view) => popovers.get(view)?.openEdit() ?? false;
192
+
193
+ function pasteUrl(view, event) {
194
+ if (event.clipboardData?.files?.length) return false;
195
+ const text = event.clipboardData?.getData("text/plain") ?? "";
196
+ if (!isUrlText(text)) return false;
197
+ const sel = view.state.selection;
198
+ if (!(sel instanceof TextSelection) || !sel.$from.sameParent(sel.$to) || sel.$from.parent.type.spec.code) return false;
199
+ const href = safeHref(text);
200
+ const tr = sel.empty
201
+ ? view.state.tr.replaceSelectionWith(schema.text(text.trim(), [LINK.create({ href })]), false)
202
+ : setLinkTr(view.state, sel.from, sel.to, href);
203
+ view.dispatch(tr.scrollIntoView().setMeta("uiEvent", "paste"));
204
+ return true;
205
+ }
206
+
207
+ export function linkPlugin() {
208
+ return new Plugin({
209
+ key: linkKey,
210
+ state: {
211
+ init: () => null,
212
+ apply(tr, value) {
213
+ const m = tr.getMeta(linkKey);
214
+ if (m !== undefined) return m;
215
+ if (!value || !tr.docChanged) return value;
216
+ const from = tr.mapping.mapResult(value.from, 1);
217
+ const to = tr.mapping.mapResult(value.to, value.from === value.to ? 1 : -1);
218
+ if (from.deletedAcross || to.deletedAcross || (value.from !== value.to && from.pos >= to.pos)) return null;
219
+ return { from: from.pos, to: to.pos };
220
+ },
221
+ },
222
+ view: (view) => new LinkPopover(view),
223
+ props: {
224
+ decorations(st) {
225
+ const r = linkKey.getState(st);
226
+ return r && r.from < r.to ? DecorationSet.create(st.doc, [Decoration.inline(r.from, r.to, { class: "page-link-pending" })]) : null;
227
+ },
228
+ handleKeyDown(view, event) {
229
+ const mod = event.metaKey || event.ctrlKey;
230
+ if (mod && !event.altKey && !event.shiftKey && event.key.toLowerCase() === "k") {
231
+ event.preventDefault();
232
+ return openLinkEditor(view) || true;
233
+ }
234
+ return false;
235
+ },
236
+ handlePaste: pasteUrl,
237
+ handleClick(view, pos, event) {
238
+ if (!(event.metaKey || event.ctrlKey)) return false;
239
+ const r = linkRange(view.state.doc, pos);
240
+ const href = safeHref(r?.href);
241
+ if (!href) return false;
242
+ view.dom.ownerDocument.defaultView.open(href, "_blank", "noopener,noreferrer");
243
+ return true;
244
+ },
245
+ },
246
+ });
247
+ }
@@ -0,0 +1,71 @@
1
+ // Markdown shortcuts typed at the start of a block: "# " to "### ", "- ", "1. ",
2
+ // "[] ", "> ", three backticks and "---", and a "| a | b |" row ended by Enter. Each retypes the block in place, keeping
3
+ // its id, pos and parent, so the change reads as a retype and not a delete and an
4
+ // insert. Backspace right after a shortcut puts the typed text back.
5
+ import { inputrules, keymap } from "../vendor/prosemirror.vendor.js";
6
+ import { schema } from "./schema.js";
7
+ import { retype, replaceWithDivider } from "./items.js";
8
+ import { asStep } from "./blocksel.js";
9
+ import { insertTableNode } from "./table.js";
10
+ import { tableFromGrid } from "./table-paste.js";
11
+
12
+ const { InputRule } = inputrules;
13
+ const N = schema.nodes;
14
+
15
+ const inlineCode = (st, start, end) => st.doc.rangeHasMark(start, end, schema.marks.code)
16
+ || !!schema.marks.code.isInSet(st.storedMarks || st.selection.$from.marks());
17
+
18
+ const blockRule = (re, item, from = ["paragraph"], entire = false) => new InputRule(re, (st, match, start, end) => {
19
+ const $s = st.doc.resolve(start);
20
+ if (!st.selection.empty || $s.depth !== 1 || $s.parentOffset !== 0 || !from.includes($s.parent.type.name)) return null;
21
+ if ((entire && end !== $s.end()) || inlineCode(st, start, end)) return null;
22
+ return asStep(retype(st.tr.delete(start, end), $s.index(0), typeof item === "function" ? item(match) : item));
23
+ });
24
+
25
+ const dividerRule = new InputRule(/^---$/, (st, match, start, end) => {
26
+ const $s = st.doc.resolve(start);
27
+ if (!st.selection.empty || $s.depth !== 1 || $s.parent.type !== N.paragraph || $s.parentOffset !== 0 || $s.parent.content.size !== end - start) return null;
28
+ if (inlineCode(st, start, end)) return null;
29
+ return asStep(replaceWithDivider(st.tr, $s.index(0)));
30
+ });
31
+
32
+ const LISTS = ["paragraph", "list_item"];
33
+
34
+ export const MARKDOWN_RULES = [
35
+ blockRule(/^(#{1,3})\s$/, (m) => ({ type: "heading", attrs: { level: m[1].length } })),
36
+ blockRule(/^[-*+]\s$/, { type: "list_item", attrs: { kind: "bullet" } }, LISTS),
37
+ blockRule(/^\d+[.)]\s$/, { type: "list_item", attrs: { kind: "ordered" } }, LISTS),
38
+ blockRule(/^\[( |x|X)?\]\s$/, (m) => ({ type: "list_item", attrs: { kind: "check", checked: !!m[1] && m[1] !== " " } }), LISTS),
39
+ blockRule(/^>\s$/, { type: "toggle", attrs: { open: true } }),
40
+ blockRule(/^```$/, { type: "code_block" }, ["paragraph"], true),
41
+ dividerRule,
42
+ ];
43
+
44
+ export const markdownPlugin = () => inputrules.inputRules({ rules: MARKDOWN_RULES });
45
+
46
+ // "| Name | Owner |" then Enter at the end of a paragraph: the paragraph becomes
47
+ // a table with those column names and one empty row, the caret in its first cell.
48
+ const TABLE_ROW = /^\s*\|(.*)\|\s*$/;
49
+
50
+ export function tableFromMarkdownRow(st, dispatch) {
51
+ const { $from, empty } = st.selection;
52
+ if (!empty || $from.depth !== 1 || $from.parent.type !== N.paragraph || $from.parentOffset !== $from.parent.content.size) return false;
53
+ if (inlineCode(st, $from.start(), $from.end())) return false;
54
+ const match = TABLE_ROW.exec($from.parent.textContent);
55
+ if (!match) return false;
56
+ const names = match[1].split("|").map((name) => name.trim());
57
+ if (!names.some(Boolean)) return false;
58
+ const table = tableFromGrid([names, names.map(() => "")], { header: true });
59
+ return insertTableNode(st, dispatch, table, { from: $from.start(), to: $from.end() });
60
+ }
61
+
62
+ export const markdownKeys = () => keymap.keymap({ Enter: tableFromMarkdownRow });
63
+
64
+ export function backspaceToParagraph(st, dispatch) {
65
+ const { $from, empty } = st.selection;
66
+ if (!empty || $from.depth !== 1 || $from.parentOffset !== 0) return false;
67
+ const n = $from.parent;
68
+ if (!(n.type === N.heading || n.type === N.callout || (n.type === N.code_block && n.content.size === 0))) return false;
69
+ if (dispatch) dispatch(asStep(st.tr.setNodeMarkup($from.before(), N.paragraph, { id: n.attrs.id, pos: n.attrs.pos, parent: n.attrs.parent })));
70
+ return true;
71
+ }