leepi 0.0.2 → 0.0.3

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 (53) hide show
  1. package/dist/core/active-marks.d.ts +15 -0
  2. package/dist/core/active-marks.js +57 -0
  3. package/dist/core/commands.d.ts +39 -0
  4. package/dist/core/commands.js +415 -0
  5. package/dist/core/editor.d.ts +25 -0
  6. package/dist/core/editor.js +102 -0
  7. package/dist/core/field-notifier.d.ts +21 -0
  8. package/dist/core/field-notifier.js +56 -0
  9. package/dist/core/highlight-style.js +60 -0
  10. package/dist/core/highlight.d.ts +8 -0
  11. package/dist/core/highlight.js +34 -0
  12. package/dist/core/plugins/blockquote.d.ts +11 -0
  13. package/dist/core/plugins/blockquote.js +78 -0
  14. package/dist/core/plugins/bracket.d.ts +6 -0
  15. package/dist/core/plugins/bracket.js +38 -0
  16. package/dist/core/plugins/code-block.d.ts +27 -0
  17. package/dist/core/plugins/code-block.js +207 -0
  18. package/dist/core/plugins/heading.d.ts +13 -0
  19. package/dist/core/plugins/heading.js +111 -0
  20. package/dist/core/plugins/inline.d.ts +14 -0
  21. package/dist/core/plugins/inline.js +103 -0
  22. package/dist/core/plugins/link.d.ts +25 -0
  23. package/dist/core/plugins/link.js +104 -0
  24. package/dist/core/plugins/list.d.ts +14 -0
  25. package/dist/core/plugins/list.js +91 -0
  26. package/dist/core/plugins/table.d.ts +12 -0
  27. package/dist/core/plugins/table.js +161 -0
  28. package/dist/core/plugins.d.ts +9 -0
  29. package/dist/core/plugins.js +9 -0
  30. package/dist/core/popover.d.ts +9 -0
  31. package/dist/core/popover.js +16 -0
  32. package/dist/core/registry.d.ts +10 -0
  33. package/dist/core/registry.js +8 -0
  34. package/dist/core/types.d.ts +25 -0
  35. package/dist/core/types.js +0 -0
  36. package/dist/core/utils.d.ts +13 -0
  37. package/dist/core/utils.js +32 -0
  38. package/dist/leepi.css +461 -0
  39. package/dist/react/code-block-popover.d.ts +76 -0
  40. package/dist/react/code-block-popover.js +223 -0
  41. package/dist/react/context.d.ts +42 -0
  42. package/dist/react/context.js +88 -0
  43. package/dist/react/editor.d.ts +30 -0
  44. package/dist/react/editor.js +60 -0
  45. package/dist/react/floating-toolbar.d.ts +30 -0
  46. package/dist/react/floating-toolbar.js +87 -0
  47. package/dist/react/link-popover.d.ts +70 -0
  48. package/dist/react/link-popover.js +222 -0
  49. package/dist/react/preview.d.ts +13 -0
  50. package/dist/react/preview.js +56 -0
  51. package/dist/react/toolbar.d.ts +51 -0
  52. package/dist/react/toolbar.js +161 -0
  53. package/package.json +1 -1
@@ -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,415 @@
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
+ function detectListKind(state, pos, contentText) {
213
+ let node = syntaxTree(state).resolveInner(pos, 1);
214
+ const indent = contentText.match(/^(\s*)/)[1];
215
+ while (node) {
216
+ if (node.name === "ListItem" && node.getChild("Task")) return {
217
+ kind: "task",
218
+ indent
219
+ };
220
+ if (node.name === "BulletList" || node.name === "OrderedList") return {
221
+ kind: node.name === "BulletList" ? "ul" : "ol",
222
+ indent
223
+ };
224
+ if (!node.parent) break;
225
+ node = node.parent;
226
+ }
227
+ return {
228
+ kind: "none",
229
+ indent: ""
230
+ };
231
+ }
232
+ function stripListPrefix(lineText) {
233
+ return lineText.replace(/^(\s*)(?:[-*+]\s\[([ xX])\]\s|[-*+]\s|\d+\.\s)/, "$1");
234
+ }
235
+ function toggleListKind(target) {
236
+ return (view) => {
237
+ if (isInsideCodeBlock(view)) return false;
238
+ const { state } = view;
239
+ const changes = state.changeByRange((range) => {
240
+ const fromLine = state.doc.lineAt(range.from);
241
+ const toLine = state.doc.lineAt(range.to);
242
+ const edits = [];
243
+ let olIdx = 1;
244
+ for (let num = fromLine.number; num <= toLine.number; num++) {
245
+ const line = state.doc.line(num);
246
+ const bqOffset = getBlockContentOffset(state, line.from, line.to);
247
+ const content = line.text.slice(bqOffset);
248
+ const { kind, indent } = detectListKind(state, line.from + bqOffset, content);
249
+ const stripped = stripListPrefix(content);
250
+ let newPrefix;
251
+ if (kind === target) newPrefix = indent;
252
+ else switch (target) {
253
+ case "ul":
254
+ newPrefix = `${indent}- `;
255
+ break;
256
+ case "ol":
257
+ newPrefix = `${indent}${olIdx++}. `;
258
+ break;
259
+ case "task":
260
+ newPrefix = `${indent}- [ ] `;
261
+ break;
262
+ default: newPrefix = indent;
263
+ }
264
+ const newContent = newPrefix + stripped.slice(indent.length);
265
+ const newLine = line.text.slice(0, bqOffset) + newContent;
266
+ if (newLine !== line.text) edits.push({
267
+ from: line.from,
268
+ to: line.to,
269
+ insert: newLine
270
+ });
271
+ }
272
+ if (edits.length === 0) return {
273
+ changes: [],
274
+ range
275
+ };
276
+ const firstDelta = edits[0].insert.length - (edits[0].to - edits[0].from);
277
+ const totalDelta = edits.reduce((sum, e) => sum + e.insert.length - (e.to - e.from), 0);
278
+ const newFrom = Math.max(0, range.from + firstDelta);
279
+ return {
280
+ changes: edits,
281
+ range: range.empty ? EditorSelection.cursor(newFrom) : EditorSelection.range(newFrom, Math.max(newFrom, range.to + totalDelta))
282
+ };
283
+ });
284
+ view.dispatch(changes);
285
+ return true;
286
+ };
287
+ }
288
+ function toggleTaskCheck(view) {
289
+ if (isInsideCodeBlock(view)) return false;
290
+ const { state } = view;
291
+ const changes = state.changeByRange((range) => {
292
+ const fromLine = state.doc.lineAt(range.from);
293
+ const toLine = state.doc.lineAt(range.to);
294
+ const edits = [];
295
+ for (let num = fromLine.number; num <= toLine.number; num++) {
296
+ const line = state.doc.line(num);
297
+ const match = line.text.match(TASK_RE);
298
+ if (!match) continue;
299
+ const isChecked = match[2] !== " ";
300
+ const bracketStart = line.from + line.text.indexOf("[");
301
+ edits.push({
302
+ from: bracketStart,
303
+ to: bracketStart + 3,
304
+ insert: isChecked ? "[ ]" : "[x]"
305
+ });
306
+ }
307
+ if (edits.length === 0) return {
308
+ changes: [],
309
+ range
310
+ };
311
+ return {
312
+ changes: edits,
313
+ range
314
+ };
315
+ });
316
+ view.dispatch(changes);
317
+ return true;
318
+ }
319
+ function findLinkAtCursor(state, pos) {
320
+ let node = syntaxTree(state).resolveInner(pos, -1);
321
+ while (node) {
322
+ if (node.name === "Link") {
323
+ const from = node.from;
324
+ const to = node.to;
325
+ const full = state.sliceDoc(from, to);
326
+ const labelStart = full.indexOf("[");
327
+ const labelEnd = full.indexOf("]");
328
+ const urlStart = full.indexOf("(", labelEnd);
329
+ const urlEnd = full.lastIndexOf(")");
330
+ if (labelStart === -1 || labelEnd === -1 || urlStart === -1 || urlEnd === -1) return null;
331
+ return {
332
+ from,
333
+ to,
334
+ label: full.slice(labelStart + 1, labelEnd),
335
+ url: full.slice(urlStart + 1, urlEnd)
336
+ };
337
+ }
338
+ if (!node.parent) break;
339
+ node = node.parent;
340
+ }
341
+ return null;
342
+ }
343
+ /** Insert a markdown link into the editor, replacing `from..to`. */
344
+ function insertLink(view, from, to, label, url) {
345
+ const insert = `[${label}](${url})`;
346
+ view.dispatch({
347
+ changes: {
348
+ from,
349
+ to,
350
+ insert
351
+ },
352
+ selection: EditorSelection.cursor(from + insert.length)
353
+ });
354
+ view.focus();
355
+ }
356
+ function parseFenceInfo(info) {
357
+ const parts = info.trim().split(/\s+/);
358
+ let lang = "";
359
+ let filename = "";
360
+ for (const part of parts) if (part.startsWith("filename=")) filename = part.slice(9);
361
+ else if (!lang) lang = part;
362
+ return {
363
+ lang,
364
+ filename
365
+ };
366
+ }
367
+ function findCodeFenceAtCursor(view) {
368
+ const { state } = view;
369
+ const pos = state.selection.main.from;
370
+ let node = syntaxTree(state).resolveInner(pos, -1);
371
+ while (node) {
372
+ if (node.name === "FencedCode") {
373
+ const fenceLine = state.doc.lineAt(node.from);
374
+ const infoNode = node.getChild("CodeInfo");
375
+ const { lang, filename } = parseFenceInfo(infoNode ? state.sliceDoc(infoNode.from, infoNode.to) : "");
376
+ return {
377
+ fenceLine: {
378
+ from: fenceLine.from,
379
+ to: fenceLine.to
380
+ },
381
+ lang,
382
+ filename
383
+ };
384
+ }
385
+ if (!node.parent) break;
386
+ node = node.parent;
387
+ }
388
+ return null;
389
+ }
390
+ /** Apply code block settings. For existing blocks, replaces the fence line. For new blocks, inserts a fenced block. */
391
+ function applyCodeBlock(view, data, lang, filename) {
392
+ const info = [lang, filename ? `filename=${filename}` : ""].filter(Boolean).join(" ");
393
+ if (data.isNew) {
394
+ const insert = `\`\`\`${info}\n\n\`\`\``;
395
+ const pos = data.insertPos;
396
+ view.dispatch({
397
+ changes: {
398
+ from: pos,
399
+ to: pos,
400
+ insert
401
+ },
402
+ selection: EditorSelection.cursor(pos + 3 + info.length + 1)
403
+ });
404
+ } else {
405
+ const fenceLine = `\`\`\`${info}`;
406
+ view.dispatch({ changes: {
407
+ from: data.fenceFrom,
408
+ to: data.fenceTo,
409
+ insert: fenceLine
410
+ } });
411
+ }
412
+ view.focus();
413
+ }
414
+ //#endregion
415
+ 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 };
@@ -0,0 +1,21 @@
1
+ import { EditorView } from "@codemirror/view";
2
+ import { Extension, StateField } from "@codemirror/state";
3
+
4
+ //#region src/core/field-notifier.d.ts
5
+ type FieldListener = () => void;
6
+ /**
7
+ * Subscribe to changes of a StateField on a given EditorView.
8
+ * Returns an unsubscribe function.
9
+ */
10
+ declare function subscribeToField<T>(view: EditorView, field: StateField<T>, cb: FieldListener): () => void;
11
+ /** Get the current snapshot of a StateField for useSyncExternalStore */
12
+ declare function getFieldSnapshot<T>(view: EditorView, field: StateField<T>): T | undefined;
13
+ /**
14
+ * ViewPlugin that watches all StateFields with subscribers and notifies
15
+ * when their values change (by reference).
16
+ */
17
+ declare const fieldNotifierPlugin: Extension;
18
+ /** Extension to include in createEditorState */
19
+ declare function fieldNotifier(): Extension;
20
+ //#endregion
21
+ export { fieldNotifier, fieldNotifierPlugin, getFieldSnapshot, subscribeToField };