leepi 0.0.2 → 0.0.4

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 (58) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/core/active-marks.d.ts +15 -0
  3. package/dist/core/active-marks.js +57 -0
  4. package/dist/core/commands.d.ts +39 -0
  5. package/dist/core/commands.js +421 -0
  6. package/dist/core/editor.d.ts +25 -0
  7. package/dist/core/editor.js +102 -0
  8. package/dist/core/field-notifier.d.ts +21 -0
  9. package/dist/core/field-notifier.js +56 -0
  10. package/dist/core/highlight-style.js +60 -0
  11. package/dist/core/highlight.d.ts +8 -0
  12. package/dist/core/highlight.js +34 -0
  13. package/dist/core/plugins/blockquote.d.ts +11 -0
  14. package/dist/core/plugins/blockquote.js +78 -0
  15. package/dist/core/plugins/bracket.d.ts +6 -0
  16. package/dist/core/plugins/bracket.js +38 -0
  17. package/dist/core/plugins/code-block.d.ts +27 -0
  18. package/dist/core/plugins/code-block.js +208 -0
  19. package/dist/core/plugins/heading.d.ts +13 -0
  20. package/dist/core/plugins/heading.js +111 -0
  21. package/dist/core/plugins/inline.d.ts +14 -0
  22. package/dist/core/plugins/inline.js +103 -0
  23. package/dist/core/plugins/link.d.ts +24 -0
  24. package/dist/core/plugins/link.js +93 -0
  25. package/dist/core/plugins/list.d.ts +14 -0
  26. package/dist/core/plugins/list.js +91 -0
  27. package/dist/core/plugins/table.d.ts +12 -0
  28. package/dist/core/plugins/table.js +161 -0
  29. package/dist/core/plugins.d.ts +9 -0
  30. package/dist/core/plugins.js +9 -0
  31. package/dist/core/popover.d.ts +9 -0
  32. package/dist/core/popover.js +16 -0
  33. package/dist/core/registry.d.ts +10 -0
  34. package/dist/core/registry.js +8 -0
  35. package/dist/core/types.d.ts +25 -0
  36. package/dist/core/types.js +0 -0
  37. package/dist/core/utils.d.ts +13 -0
  38. package/dist/core/utils.js +32 -0
  39. package/dist/leepi.css +461 -0
  40. package/dist/react/code-block-popover.d.ts +76 -0
  41. package/dist/react/code-block-popover.js +223 -0
  42. package/dist/react/context.d.ts +42 -0
  43. package/dist/react/context.js +88 -0
  44. package/dist/react/editor.d.ts +30 -0
  45. package/dist/react/editor.js +60 -0
  46. package/dist/react/floating-toolbar.d.ts +30 -0
  47. package/dist/react/floating-toolbar.js +87 -0
  48. package/dist/react/link-popover.d.ts +70 -0
  49. package/dist/react/link-popover.js +222 -0
  50. package/dist/react/preview.d.ts +13 -0
  51. package/dist/react/preview.js +56 -0
  52. package/dist/react/toolbar.d.ts +51 -0
  53. package/dist/react/toolbar.js +161 -0
  54. package/package.json +3 -2
  55. package/src/core/commands.ts +12 -1
  56. package/src/core/plugins/code-block.ts +1 -0
  57. package/src/core/plugins/link.ts +7 -24
  58. package/src/react/toolbar.tsx +3 -3
package/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # leepi
2
+
3
+ ## 0.0.4
4
+
5
+ ### Patch Changes
6
+
7
+ - e25ae8e: Fix bug with ul/ol toggles
8
+ - e25ae8e: Use link paste primitive from library instead of custom
9
+
10
+ ## 0.0.3
11
+
12
+ ### Patch Changes
13
+
14
+ - 2aa717f: Build package first before publishing
15
+
16
+ ## 0.0.2
17
+
18
+ ### Patch Changes
19
+
20
+ - 5778861: A headless markdown editor using CodeMirror with keyboard shortcuts and built-in React primitives
@@ -0,0 +1,15 @@
1
+ import { EditorView } from "@codemirror/view";
2
+ import { Extension } from "@codemirror/state";
3
+
4
+ //#region src/core/active-marks.d.ts
5
+ /**
6
+ * Active marks as a dynamic record. Keys are mark names registered via `markRegistry`.
7
+ */
8
+ type ActiveMarks = Record<string, boolean>;
9
+ declare const emptyMarks: ActiveMarks;
10
+ declare function detectMarks(view: EditorView): ActiveMarks;
11
+ declare function subscribeToMarks(view: EditorView, callback: () => void): () => void;
12
+ declare function getMarksSnapshot(view: EditorView): ActiveMarks;
13
+ declare const activeMarksPlugin: Extension;
14
+ //#endregion
15
+ export { ActiveMarks, activeMarksPlugin, detectMarks, emptyMarks, getMarksSnapshot, subscribeToMarks };
@@ -0,0 +1,57 @@
1
+ import { markRegistry } from "./registry.js";
2
+ import { ViewPlugin } from "@codemirror/view";
3
+ import { syntaxTree } from "@codemirror/language";
4
+ //#region src/core/active-marks.ts
5
+ const emptyMarks = {};
6
+ function detectMarks(view) {
7
+ const { state } = view;
8
+ const pos = state.selection.main.from;
9
+ const tree = syntaxTree(state);
10
+ const detectors = state.facet(markRegistry);
11
+ const marks = {};
12
+ let node = tree.resolveInner(pos, -1);
13
+ while (node) {
14
+ for (const det of detectors) if (!marks[det.mark] && det.detect(node.name, state, pos)) marks[det.mark] = true;
15
+ if (!node.parent) break;
16
+ node = node.parent;
17
+ }
18
+ for (const det of detectors) if (!marks[det.mark] && det.detect("__line__", state, pos)) marks[det.mark] = true;
19
+ return marks;
20
+ }
21
+ const viewListeners = /* @__PURE__ */ new WeakMap();
22
+ const viewSnapshots = /* @__PURE__ */ new WeakMap();
23
+ function subscribeToMarks(view, callback) {
24
+ let listeners = viewListeners.get(view);
25
+ if (!listeners) {
26
+ listeners = /* @__PURE__ */ new Set();
27
+ viewListeners.set(view, listeners);
28
+ }
29
+ listeners.add(callback);
30
+ return () => {
31
+ listeners.delete(callback);
32
+ };
33
+ }
34
+ function getMarksSnapshot(view) {
35
+ return viewSnapshots.get(view) ?? emptyMarks;
36
+ }
37
+ function marksEqual(a, b) {
38
+ const aKeys = Object.keys(a);
39
+ const bKeys = Object.keys(b);
40
+ if (aKeys.length !== bKeys.length) return false;
41
+ for (const key of aKeys) if (a[key] !== b[key]) return false;
42
+ return true;
43
+ }
44
+ const activeMarksPlugin = ViewPlugin.fromClass(class {
45
+ update(update) {
46
+ if (update.selectionSet || update.docChanged) {
47
+ const marks = detectMarks(update.view);
48
+ if (!marksEqual(viewSnapshots.get(update.view) ?? emptyMarks, marks)) {
49
+ viewSnapshots.set(update.view, marks);
50
+ const listeners = viewListeners.get(update.view);
51
+ if (listeners) for (const cb of listeners) cb();
52
+ }
53
+ }
54
+ }
55
+ });
56
+ //#endregion
57
+ export { activeMarksPlugin, detectMarks, emptyMarks, getMarksSnapshot, subscribeToMarks };
@@ -0,0 +1,39 @@
1
+ import { CodeBlockData } from "./plugins/code-block.js";
2
+ import { EditorView } from "@codemirror/view";
3
+ import { EditorState } from "@codemirror/state";
4
+
5
+ //#region src/core/commands.d.ts
6
+ /**
7
+ * Toggle an inline markdown marker (e.g. `**`, `_`, `` ` ``, `~~`) around
8
+ * each selection range. If the selected text is already wrapped with the
9
+ * marker, the marker is removed; otherwise it is added.
10
+ *
11
+ * When nothing is selected the marker pair is inserted at the cursor and the
12
+ * cursor is placed between them.
13
+ */
14
+ declare function toggleMarker(marker: string): (view: EditorView) => boolean;
15
+ declare function toggleBlockquote(view: EditorView): boolean;
16
+ declare function toggleHeading(level: number): (view: EditorView) => boolean;
17
+ type ListKind = "ul" | "ol" | "task" | "none";
18
+ declare function toggleListKind(target: ListKind): (view: EditorView) => boolean;
19
+ declare function toggleTaskCheck(view: EditorView): boolean;
20
+ declare function findLinkAtCursor(state: EditorState, pos: number): {
21
+ from: number;
22
+ to: number;
23
+ label: string;
24
+ url: string;
25
+ } | null;
26
+ /** Insert a markdown link into the editor, replacing `from..to`. */
27
+ declare function insertLink(view: EditorView, from: number, to: number, label: string, url: string): void;
28
+ declare function findCodeFenceAtCursor(view: EditorView): {
29
+ fenceLine: {
30
+ from: number;
31
+ to: number;
32
+ };
33
+ lang: string;
34
+ filename: string;
35
+ } | null;
36
+ /** Apply code block settings. For existing blocks, replaces the fence line. For new blocks, inserts a fenced block. */
37
+ declare function applyCodeBlock(view: EditorView, data: CodeBlockData, lang: string, filename: string): void;
38
+ //#endregion
39
+ export { applyCodeBlock, findCodeFenceAtCursor, findLinkAtCursor, insertLink, toggleBlockquote, toggleHeading, toggleListKind, toggleMarker, toggleTaskCheck };
@@ -0,0 +1,421 @@
1
+ import { isInsideCodeBlock } from "./utils.js";
2
+ import { syntaxTree } from "@codemirror/language";
3
+ import { EditorSelection } from "@codemirror/state";
4
+ //#region src/core/commands.ts
5
+ /**
6
+ * Toggle an inline markdown marker (e.g. `**`, `_`, `` ` ``, `~~`) around
7
+ * each selection range. If the selected text is already wrapped with the
8
+ * marker, the marker is removed; otherwise it is added.
9
+ *
10
+ * When nothing is selected the marker pair is inserted at the cursor and the
11
+ * cursor is placed between them.
12
+ */
13
+ function toggleMarker(marker) {
14
+ return (view) => {
15
+ if (isInsideCodeBlock(view)) return false;
16
+ const { state } = view;
17
+ const len = marker.length;
18
+ const changes = state.changeByRange((range) => {
19
+ const doc = state.doc;
20
+ if (range.empty) {
21
+ const line = doc.lineAt(range.from);
22
+ const lineText = line.text;
23
+ const cursorOffset = range.from - line.from;
24
+ const positions = [];
25
+ let searchFrom = 0;
26
+ while (searchFrom <= lineText.length - len) {
27
+ const idx = lineText.indexOf(marker, searchFrom);
28
+ if (idx === -1) break;
29
+ positions.push(idx);
30
+ searchFrom = idx + len;
31
+ }
32
+ for (let i = 0; i + 1 < positions.length; i += 2) {
33
+ const openEnd = positions[i] + len;
34
+ const closeStart = positions[i + 1];
35
+ if (cursorOffset >= openEnd && cursorOffset <= closeStart) {
36
+ const absOpen = line.from + positions[i];
37
+ const absClose = line.from + closeStart;
38
+ const innerText = doc.sliceString(absOpen + len, absClose);
39
+ const cursorInInner = range.from - (absOpen + len);
40
+ return {
41
+ changes: {
42
+ from: absOpen,
43
+ to: absClose + len,
44
+ insert: innerText
45
+ },
46
+ range: EditorSelection.cursor(absOpen + cursorInInner)
47
+ };
48
+ }
49
+ }
50
+ return {
51
+ changes: {
52
+ from: range.from,
53
+ insert: marker + marker
54
+ },
55
+ range: EditorSelection.cursor(range.from + len)
56
+ };
57
+ }
58
+ const selected = doc.sliceString(range.from, range.to);
59
+ if (selected.startsWith(marker) && selected.endsWith(marker) && selected.length >= len * 2) {
60
+ const inner = selected.slice(len, -len);
61
+ return {
62
+ changes: {
63
+ from: range.from,
64
+ to: range.to,
65
+ insert: inner
66
+ },
67
+ range: EditorSelection.range(range.from, range.from + inner.length)
68
+ };
69
+ }
70
+ const beforeSel = doc.sliceString(Math.max(0, range.from - len), range.from);
71
+ const afterSel = doc.sliceString(range.to, Math.min(doc.length, range.to + len));
72
+ if (beforeSel === marker && afterSel === marker) return {
73
+ changes: [{
74
+ from: range.from - len,
75
+ to: range.from
76
+ }, {
77
+ from: range.to,
78
+ to: range.to + len
79
+ }],
80
+ range: EditorSelection.range(range.from - len, range.to - len)
81
+ };
82
+ return {
83
+ changes: [{
84
+ from: range.from,
85
+ insert: marker
86
+ }, {
87
+ from: range.to,
88
+ insert: marker
89
+ }],
90
+ range: EditorSelection.range(range.from, range.to + len * 2)
91
+ };
92
+ });
93
+ view.dispatch(changes);
94
+ return true;
95
+ };
96
+ }
97
+ const BQ_RE = /^(\s*)>\s?/;
98
+ function toggleBlockquote(view) {
99
+ if (isInsideCodeBlock(view)) return false;
100
+ const { state } = view;
101
+ const changes = state.changeByRange((range) => {
102
+ const fromLine = state.doc.lineAt(range.from);
103
+ const toLine = state.doc.lineAt(range.to);
104
+ const edits = [];
105
+ let allQuoted = true;
106
+ for (let num = fromLine.number; num <= toLine.number; num++) {
107
+ const line = state.doc.line(num);
108
+ if (!BQ_RE.test(line.text)) {
109
+ allQuoted = false;
110
+ break;
111
+ }
112
+ }
113
+ for (let num = fromLine.number; num <= toLine.number; num++) {
114
+ const line = state.doc.line(num);
115
+ if (allQuoted) {
116
+ const match = line.text.match(BQ_RE);
117
+ edits.push({
118
+ from: line.from,
119
+ to: line.from + match[0].length,
120
+ insert: match[1]
121
+ });
122
+ } else if (!BQ_RE.test(line.text)) {
123
+ const indent = line.text.match(/^(\s*)/)[1];
124
+ edits.push({
125
+ from: line.from + indent.length,
126
+ to: line.from + indent.length,
127
+ insert: "> "
128
+ });
129
+ }
130
+ }
131
+ if (edits.length === 0) return {
132
+ changes: [],
133
+ range
134
+ };
135
+ const firstDelta = edits[0].insert.length - (edits[0].to - edits[0].from);
136
+ const totalDelta = edits.reduce((sum, e) => sum + e.insert.length - (e.to - e.from), 0);
137
+ const newFrom = Math.max(0, range.from + firstDelta);
138
+ return {
139
+ changes: edits,
140
+ range: range.empty ? EditorSelection.cursor(newFrom) : EditorSelection.range(newFrom, Math.max(newFrom, range.to + totalDelta))
141
+ };
142
+ });
143
+ view.dispatch(changes);
144
+ return true;
145
+ }
146
+ /**
147
+ * Find the offset where content begins after any blockquote markers on a line.
148
+ * Uses the syntax tree to detect `QuoteMark` nodes, handling nested blockquotes.
149
+ */
150
+ function getBlockContentOffset(state, lineFrom, lineTo) {
151
+ const tree = syntaxTree(state);
152
+ let lastQuoteEnd = lineFrom;
153
+ let foundQuote = false;
154
+ tree.iterate({
155
+ from: lineFrom,
156
+ to: lineTo,
157
+ enter(node) {
158
+ if (node.name === "QuoteMark" && node.from >= lineFrom && node.to <= lineTo) {
159
+ foundQuote = true;
160
+ lastQuoteEnd = node.to;
161
+ }
162
+ }
163
+ });
164
+ if (!foundQuote) return 0;
165
+ const doc = state.doc;
166
+ let offset = lastQuoteEnd;
167
+ while (offset < lineTo && doc.sliceString(offset, offset + 1) === " ") offset++;
168
+ return offset - lineFrom;
169
+ }
170
+ function toggleHeading(level) {
171
+ return (view) => {
172
+ if (isInsideCodeBlock(view)) return false;
173
+ const { state } = view;
174
+ const line = state.doc.lineAt(state.selection.main.from);
175
+ const contentOffset = getBlockContentOffset(state, line.from, line.to);
176
+ const headingMatch = line.text.slice(contentOffset).match(/^(#{1,6})\s/);
177
+ const prefix = "#".repeat(level) + " ";
178
+ const contentFrom = line.from + contentOffset;
179
+ const cursorPos = state.selection.main.from;
180
+ if (headingMatch && headingMatch[1].length === level) {
181
+ const delta = -headingMatch[0].length;
182
+ view.dispatch({
183
+ changes: {
184
+ from: contentFrom,
185
+ to: contentFrom + headingMatch[0].length,
186
+ insert: ""
187
+ },
188
+ selection: EditorSelection.cursor(Math.max(contentFrom, cursorPos + delta))
189
+ });
190
+ } else if (headingMatch) {
191
+ const delta = prefix.length - headingMatch[0].length;
192
+ view.dispatch({
193
+ changes: {
194
+ from: contentFrom,
195
+ to: contentFrom + headingMatch[0].length,
196
+ insert: prefix
197
+ },
198
+ selection: EditorSelection.cursor(Math.max(contentFrom + prefix.length, cursorPos + delta))
199
+ });
200
+ } else view.dispatch({
201
+ changes: {
202
+ from: contentFrom,
203
+ to: contentFrom,
204
+ insert: prefix
205
+ },
206
+ selection: EditorSelection.cursor(Math.max(contentFrom + prefix.length, cursorPos + prefix.length))
207
+ });
208
+ return true;
209
+ };
210
+ }
211
+ const TASK_RE = /^(\s*)[-*+]\s\[([ xX])\]\s/;
212
+ const UL_RE = /^(\s*)[-*+]\s/;
213
+ const OL_RE = /^(\s*)\d+\.\s/;
214
+ function detectListKind(state, pos, contentText) {
215
+ const indent = contentText.match(/^(\s*)/)[1];
216
+ if (!UL_RE.test(contentText) && !OL_RE.test(contentText) && !TASK_RE.test(contentText)) return {
217
+ kind: "none",
218
+ indent: ""
219
+ };
220
+ let node = syntaxTree(state).resolveInner(pos, 1);
221
+ while (node) {
222
+ if (node.name === "ListItem" && node.getChild("Task")) return {
223
+ kind: "task",
224
+ indent
225
+ };
226
+ if (node.name === "BulletList" || node.name === "OrderedList") return {
227
+ kind: node.name === "BulletList" ? "ul" : "ol",
228
+ indent
229
+ };
230
+ if (!node.parent) break;
231
+ node = node.parent;
232
+ }
233
+ return {
234
+ kind: "none",
235
+ indent: ""
236
+ };
237
+ }
238
+ function stripListPrefix(lineText) {
239
+ return lineText.replace(/^(\s*)(?:[-*+]\s\[([ xX])\]\s|[-*+]\s|\d+\.\s)/, "$1");
240
+ }
241
+ function toggleListKind(target) {
242
+ return (view) => {
243
+ if (isInsideCodeBlock(view)) return false;
244
+ const { state } = view;
245
+ const changes = state.changeByRange((range) => {
246
+ const fromLine = state.doc.lineAt(range.from);
247
+ const toLine = state.doc.lineAt(range.to);
248
+ const edits = [];
249
+ let olIdx = 1;
250
+ for (let num = fromLine.number; num <= toLine.number; num++) {
251
+ const line = state.doc.line(num);
252
+ const bqOffset = getBlockContentOffset(state, line.from, line.to);
253
+ const content = line.text.slice(bqOffset);
254
+ const { kind, indent } = detectListKind(state, line.from + bqOffset, content);
255
+ const stripped = stripListPrefix(content);
256
+ let newPrefix;
257
+ if (kind === target) newPrefix = indent;
258
+ else switch (target) {
259
+ case "ul":
260
+ newPrefix = `${indent}- `;
261
+ break;
262
+ case "ol":
263
+ newPrefix = `${indent}${olIdx++}. `;
264
+ break;
265
+ case "task":
266
+ newPrefix = `${indent}- [ ] `;
267
+ break;
268
+ default: newPrefix = indent;
269
+ }
270
+ const newContent = newPrefix + stripped.slice(indent.length);
271
+ const newLine = line.text.slice(0, bqOffset) + newContent;
272
+ if (newLine !== line.text) edits.push({
273
+ from: line.from,
274
+ to: line.to,
275
+ insert: newLine
276
+ });
277
+ }
278
+ if (edits.length === 0) return {
279
+ changes: [],
280
+ range
281
+ };
282
+ const firstDelta = edits[0].insert.length - (edits[0].to - edits[0].from);
283
+ const totalDelta = edits.reduce((sum, e) => sum + e.insert.length - (e.to - e.from), 0);
284
+ const newFrom = Math.max(0, range.from + firstDelta);
285
+ return {
286
+ changes: edits,
287
+ range: range.empty ? EditorSelection.cursor(newFrom) : EditorSelection.range(newFrom, Math.max(newFrom, range.to + totalDelta))
288
+ };
289
+ });
290
+ view.dispatch(changes);
291
+ return true;
292
+ };
293
+ }
294
+ function toggleTaskCheck(view) {
295
+ if (isInsideCodeBlock(view)) return false;
296
+ const { state } = view;
297
+ const changes = state.changeByRange((range) => {
298
+ const fromLine = state.doc.lineAt(range.from);
299
+ const toLine = state.doc.lineAt(range.to);
300
+ const edits = [];
301
+ for (let num = fromLine.number; num <= toLine.number; num++) {
302
+ const line = state.doc.line(num);
303
+ const match = line.text.match(TASK_RE);
304
+ if (!match) continue;
305
+ const isChecked = match[2] !== " ";
306
+ const bracketStart = line.from + line.text.indexOf("[");
307
+ edits.push({
308
+ from: bracketStart,
309
+ to: bracketStart + 3,
310
+ insert: isChecked ? "[ ]" : "[x]"
311
+ });
312
+ }
313
+ if (edits.length === 0) return {
314
+ changes: [],
315
+ range
316
+ };
317
+ return {
318
+ changes: edits,
319
+ range
320
+ };
321
+ });
322
+ view.dispatch(changes);
323
+ return true;
324
+ }
325
+ function findLinkAtCursor(state, pos) {
326
+ let node = syntaxTree(state).resolveInner(pos, -1);
327
+ while (node) {
328
+ if (node.name === "Link") {
329
+ const from = node.from;
330
+ const to = node.to;
331
+ const full = state.sliceDoc(from, to);
332
+ const labelStart = full.indexOf("[");
333
+ const labelEnd = full.indexOf("]");
334
+ const urlStart = full.indexOf("(", labelEnd);
335
+ const urlEnd = full.lastIndexOf(")");
336
+ if (labelStart === -1 || labelEnd === -1 || urlStart === -1 || urlEnd === -1) return null;
337
+ return {
338
+ from,
339
+ to,
340
+ label: full.slice(labelStart + 1, labelEnd),
341
+ url: full.slice(urlStart + 1, urlEnd)
342
+ };
343
+ }
344
+ if (!node.parent) break;
345
+ node = node.parent;
346
+ }
347
+ return null;
348
+ }
349
+ /** Insert a markdown link into the editor, replacing `from..to`. */
350
+ function insertLink(view, from, to, label, url) {
351
+ const insert = `[${label}](${url})`;
352
+ view.dispatch({
353
+ changes: {
354
+ from,
355
+ to,
356
+ insert
357
+ },
358
+ selection: EditorSelection.cursor(from + insert.length)
359
+ });
360
+ view.focus();
361
+ }
362
+ function parseFenceInfo(info) {
363
+ const parts = info.trim().split(/\s+/);
364
+ let lang = "";
365
+ let filename = "";
366
+ for (const part of parts) if (part.startsWith("filename=")) filename = part.slice(9);
367
+ else if (!lang) lang = part;
368
+ return {
369
+ lang,
370
+ filename
371
+ };
372
+ }
373
+ function findCodeFenceAtCursor(view) {
374
+ const { state } = view;
375
+ const pos = state.selection.main.from;
376
+ let node = syntaxTree(state).resolveInner(pos, -1);
377
+ while (node) {
378
+ if (node.name === "FencedCode") {
379
+ const fenceLine = state.doc.lineAt(node.from);
380
+ const infoNode = node.getChild("CodeInfo");
381
+ const { lang, filename } = parseFenceInfo(infoNode ? state.sliceDoc(infoNode.from, infoNode.to) : "");
382
+ return {
383
+ fenceLine: {
384
+ from: fenceLine.from,
385
+ to: fenceLine.to
386
+ },
387
+ lang,
388
+ filename
389
+ };
390
+ }
391
+ if (!node.parent) break;
392
+ node = node.parent;
393
+ }
394
+ return null;
395
+ }
396
+ /** Apply code block settings. For existing blocks, replaces the fence line. For new blocks, inserts a fenced block. */
397
+ function applyCodeBlock(view, data, lang, filename) {
398
+ const info = [lang, filename ? `filename=${filename}` : ""].filter(Boolean).join(" ");
399
+ if (data.isNew) {
400
+ const insert = `\`\`\`${info}\n\n\`\`\``;
401
+ const pos = data.insertPos;
402
+ view.dispatch({
403
+ changes: {
404
+ from: pos,
405
+ to: pos,
406
+ insert
407
+ },
408
+ selection: EditorSelection.cursor(pos + 3 + info.length + 1)
409
+ });
410
+ } else {
411
+ const fenceLine = `\`\`\`${info}`;
412
+ view.dispatch({ changes: {
413
+ from: data.fenceFrom,
414
+ to: data.fenceTo,
415
+ insert: fenceLine
416
+ } });
417
+ }
418
+ view.focus();
419
+ }
420
+ //#endregion
421
+ export { applyCodeBlock, findCodeFenceAtCursor, findLinkAtCursor, insertLink, toggleBlockquote, toggleHeading, toggleListKind, toggleMarker, toggleTaskCheck };
@@ -0,0 +1,25 @@
1
+ import { EditorView } from "@codemirror/view";
2
+ import { Compartment, EditorState, Extension } from "@codemirror/state";
3
+
4
+ //#region src/core/editor.d.ts
5
+ interface CreateEditorStateOptions {
6
+ doc?: string;
7
+ onUpdate?: (doc: string) => void;
8
+ /** Additional extensions (e.g. custom plugins) */
9
+ plugins?: Extension[];
10
+ placeholder?: string;
11
+ readOnly?: boolean;
12
+ /** When false, default plugins are NOT included. Defaults to true. */
13
+ includeDefaultPlugins?: boolean;
14
+ }
15
+ /** Compartment for swapping the markdown extension after lazy-loading language support */
16
+ declare const markdownCompartment: Compartment;
17
+ /** Compartment for toggling editable state */
18
+ declare const editableCompartment: Compartment;
19
+ /** Default set of all plugins */
20
+ declare function defaultPlugins(): Extension;
21
+ declare function createEditorState(options: CreateEditorStateOptions): EditorState;
22
+ /** Lazy-load code language support and reconfigure the markdown extension */
23
+ declare function loadLanguageSupport(view: EditorView): void;
24
+ //#endregion
25
+ export { CreateEditorStateOptions, createEditorState, defaultPlugins, editableCompartment, loadLanguageSupport, markdownCompartment };
@@ -0,0 +1,102 @@
1
+ import { activeMarksPlugin } from "./active-marks.js";
2
+ import { editorTheme, highlightStyle } from "./highlight-style.js";
3
+ import { fieldNotifier } from "./field-notifier.js";
4
+ import { popoverField } from "./popover.js";
5
+ import { inlinePlugin } from "./plugins/inline.js";
6
+ import { headingPlugin } from "./plugins/heading.js";
7
+ import { listPlugin } from "./plugins/list.js";
8
+ import { blockquotePlugin } from "./plugins/blockquote.js";
9
+ import { linkPlugin } from "./plugins/link.js";
10
+ import { codeBlockPlugin } from "./plugins/code-block.js";
11
+ import { tablePlugin } from "./plugins/table.js";
12
+ import { bracketPlugin } from "./plugins/bracket.js";
13
+ import "./plugins.js";
14
+ import { EditorView, keymap, placeholder } from "@codemirror/view";
15
+ import { syntaxHighlighting } from "@codemirror/language";
16
+ import { Compartment, EditorSelection, EditorState } from "@codemirror/state";
17
+ import { markdown, markdownLanguage } from "@codemirror/lang-markdown";
18
+ import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
19
+ import { styleTags, tags } from "@lezer/highlight";
20
+ //#region src/core/editor.ts
21
+ /** Compartment for swapping the markdown extension after lazy-loading language support */
22
+ const markdownCompartment = new Compartment();
23
+ /** Compartment for toggling editable state */
24
+ const editableCompartment = new Compartment();
25
+ const markdownStyleTags = { props: [styleTags({
26
+ ListMark: tags.processingInstruction,
27
+ QuoteMark: tags.processingInstruction,
28
+ TaskMarker: tags.processingInstruction,
29
+ CodeText: tags.content
30
+ })] };
31
+ /** Select the word under the cursor. Does nothing if there is already a selection. */
32
+ function selectWord(view) {
33
+ const main = view.state.selection.main;
34
+ if (!main.empty) return false;
35
+ const word = view.state.wordAt(main.head);
36
+ if (!word) return false;
37
+ view.dispatch({ selection: EditorSelection.single(word.from, word.to) });
38
+ return true;
39
+ }
40
+ /** Default set of all plugins */
41
+ function defaultPlugins() {
42
+ return [
43
+ inlinePlugin(),
44
+ headingPlugin(),
45
+ listPlugin(),
46
+ blockquotePlugin(),
47
+ linkPlugin(),
48
+ codeBlockPlugin(),
49
+ tablePlugin(),
50
+ bracketPlugin()
51
+ ];
52
+ }
53
+ function createEditorState(options) {
54
+ const { doc = "", onUpdate, plugins = [], placeholder: placeholder$1 = "", readOnly = false, includeDefaultPlugins = true } = options;
55
+ const extensions = [
56
+ editableCompartment.of(EditorView.editable.of(!readOnly)),
57
+ ...includeDefaultPlugins ? [defaultPlugins()] : [],
58
+ ...plugins,
59
+ keymap.of([
60
+ {
61
+ key: "Mod-d",
62
+ run: selectWord,
63
+ preventDefault: true
64
+ },
65
+ ...defaultKeymap,
66
+ ...historyKeymap
67
+ ]),
68
+ history(),
69
+ popoverField,
70
+ activeMarksPlugin,
71
+ fieldNotifier(),
72
+ markdownCompartment.of(markdown({
73
+ base: markdownLanguage,
74
+ extensions: markdownStyleTags,
75
+ addKeymap: true
76
+ })),
77
+ syntaxHighlighting(highlightStyle),
78
+ editorTheme,
79
+ EditorView.lineWrapping
80
+ ];
81
+ if (placeholder$1) extensions.push(placeholder(placeholder$1));
82
+ if (onUpdate) extensions.push(EditorView.updateListener.of((update) => {
83
+ if (update.docChanged) onUpdate(update.state.doc.toString());
84
+ }));
85
+ return EditorState.create({
86
+ doc,
87
+ extensions
88
+ });
89
+ }
90
+ /** Lazy-load code language support and reconfigure the markdown extension */
91
+ function loadLanguageSupport(view) {
92
+ import("@codemirror/language-data").then(({ languages }) => {
93
+ view.dispatch({ effects: markdownCompartment.reconfigure(markdown({
94
+ base: markdownLanguage,
95
+ codeLanguages: languages,
96
+ extensions: markdownStyleTags,
97
+ addKeymap: true
98
+ })) });
99
+ });
100
+ }
101
+ //#endregion
102
+ export { createEditorState, defaultPlugins, editableCompartment, loadLanguageSupport, markdownCompartment };