draftly 0.1.0-alpha.1 → 1.0.7

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 (68) hide show
  1. package/dist/chunk-2B3A3VSQ.cjs +3382 -0
  2. package/dist/chunk-2B3A3VSQ.cjs.map +1 -0
  3. package/dist/{chunk-ZDSZRRUY.cjs → chunk-72ZYRGRT.cjs} +48 -104
  4. package/dist/chunk-72ZYRGRT.cjs.map +1 -0
  5. package/dist/{chunk-MOG6E2LY.js → chunk-CG4M4TC7.js} +46 -103
  6. package/dist/chunk-CG4M4TC7.js.map +1 -0
  7. package/dist/{chunk-LCQALOEI.js → chunk-DFQYXFOP.js} +36 -3
  8. package/dist/chunk-DFQYXFOP.js.map +1 -0
  9. package/dist/{chunk-6LQ2VR4I.js → chunk-HPSMS2WB.js} +38 -22
  10. package/dist/chunk-HPSMS2WB.js.map +1 -0
  11. package/dist/{chunk-7Z3SRTPZ.cjs → chunk-KBQDZ5IW.cjs} +38 -22
  12. package/dist/chunk-KBQDZ5IW.cjs.map +1 -0
  13. package/dist/{chunk-RV2SYFA6.cjs → chunk-KDEDLC3D.cjs} +36 -2
  14. package/dist/chunk-KDEDLC3D.cjs.map +1 -0
  15. package/dist/chunk-N3WL3XPB.js +3360 -0
  16. package/dist/chunk-N3WL3XPB.js.map +1 -0
  17. package/dist/{draftly-Bxu_H4nw.d.ts → draftly-BLnx3uGX.d.cts} +12 -6
  18. package/dist/{draftly-Bxu_H4nw.d.cts → draftly-BLnx3uGX.d.ts} +12 -6
  19. package/dist/editor/index.cjs +20 -12
  20. package/dist/editor/index.d.cts +3 -2
  21. package/dist/editor/index.d.ts +3 -2
  22. package/dist/editor/index.js +2 -2
  23. package/dist/index.cjs +59 -27
  24. package/dist/index.d.cts +3 -3
  25. package/dist/index.d.ts +3 -3
  26. package/dist/index.js +4 -4
  27. package/dist/plugins/index.cjs +35 -11
  28. package/dist/plugins/index.d.cts +321 -3
  29. package/dist/plugins/index.d.ts +321 -3
  30. package/dist/plugins/index.js +3 -3
  31. package/dist/preview/index.cjs +7 -7
  32. package/dist/preview/index.d.cts +9 -4
  33. package/dist/preview/index.d.ts +9 -4
  34. package/dist/preview/index.js +2 -2
  35. package/package.json +2 -1
  36. package/src/editor/draftly.ts +9 -13
  37. package/src/editor/plugin.ts +4 -1
  38. package/src/editor/theme.ts +34 -1
  39. package/src/editor/utils.ts +49 -0
  40. package/src/editor/view-plugin.ts +6 -131
  41. package/src/plugins/code-plugin.ts +1119 -0
  42. package/src/plugins/heading-plugin.ts +23 -11
  43. package/src/plugins/hr-plugin.ts +102 -0
  44. package/src/plugins/image-plugin.ts +96 -2
  45. package/src/plugins/index.ts +57 -39
  46. package/src/plugins/inline-plugin.ts +125 -6
  47. package/src/plugins/link-plugin.ts +509 -0
  48. package/src/plugins/list-plugin.ts +116 -2
  49. package/src/plugins/math-plugin.ts +5 -1
  50. package/src/plugins/mermaid-plugin.ts +500 -0
  51. package/src/plugins/paragraph-plugin.ts +38 -0
  52. package/src/plugins/quote-plugin.ts +146 -0
  53. package/src/preview/context.ts +1 -1
  54. package/src/preview/css-generator.ts +3 -1
  55. package/src/preview/default-renderers.ts +0 -5
  56. package/src/preview/preview.ts +2 -2
  57. package/src/preview/renderer.ts +34 -12
  58. package/src/preview/types.ts +1 -1
  59. package/dist/chunk-6LQ2VR4I.js.map +0 -1
  60. package/dist/chunk-7Z3SRTPZ.cjs.map +0 -1
  61. package/dist/chunk-GA6NYY77.cjs +0 -1400
  62. package/dist/chunk-GA6NYY77.cjs.map +0 -1
  63. package/dist/chunk-LCQALOEI.js.map +0 -1
  64. package/dist/chunk-MOG6E2LY.js.map +0 -1
  65. package/dist/chunk-RV2SYFA6.cjs.map +0 -1
  66. package/dist/chunk-TKZNKWGF.js +0 -1385
  67. package/dist/chunk-TKZNKWGF.js.map +0 -1
  68. package/dist/chunk-ZDSZRRUY.cjs.map +0 -1
@@ -0,0 +1,3360 @@
1
+ import { DraftlyPlugin, DecorationPlugin } from './chunk-CG4M4TC7.js';
2
+ import { createTheme, toggleMarkdownStyle } from './chunk-DFQYXFOP.js';
3
+ import { Decoration, WidgetType } from '@codemirror/view';
4
+ import { syntaxTree } from '@codemirror/language';
5
+ import { tags, highlightCode, classHighlighter } from '@lezer/highlight';
6
+ import DOMPurify from 'dompurify';
7
+ import katex from 'katex';
8
+ import katexCss from 'katex/dist/katex.min.css?raw';
9
+ import mermaid from 'mermaid';
10
+ import { languages } from '@codemirror/language-data';
11
+
12
+ // src/plugins/paragraph-plugin.ts
13
+ var ParagraphPlugin = class extends DraftlyPlugin {
14
+ name = "paragraph";
15
+ version = "1.0.0";
16
+ requiredNodes = ["Paragraph"];
17
+ /**
18
+ * Plugin theme for preview styling
19
+ */
20
+ get theme() {
21
+ return theme;
22
+ }
23
+ renderToHTML(node, children) {
24
+ if (node.name !== "Paragraph") {
25
+ return null;
26
+ }
27
+ return `<p class="cm-draftly-paragraph">${children}</p>`;
28
+ }
29
+ };
30
+ var theme = createTheme({
31
+ default: {
32
+ ".cm-draftly-paragraph": {
33
+ paddingTop: "0.5em",
34
+ paddingBottom: "0.5em"
35
+ }
36
+ }
37
+ });
38
+ var HEADING_TYPES = ["ATXHeading1", "ATXHeading2", "ATXHeading3", "ATXHeading4", "ATXHeading5", "ATXHeading6"];
39
+ var headingMarkDecorations = {
40
+ "heading-1": Decoration.mark({ class: "cm-draftly-h1" }),
41
+ "heading-2": Decoration.mark({ class: "cm-draftly-h2" }),
42
+ "heading-3": Decoration.mark({ class: "cm-draftly-h3" }),
43
+ "heading-4": Decoration.mark({ class: "cm-draftly-h4" }),
44
+ "heading-5": Decoration.mark({ class: "cm-draftly-h5" }),
45
+ "heading-6": Decoration.mark({ class: "cm-draftly-h6" }),
46
+ "header-mark-class": Decoration.mark({ class: "cm-draftly-header-mark" }),
47
+ "heading-mark": Decoration.replace({})
48
+ };
49
+ var headingLineDecorations = {
50
+ "heading-1": Decoration.line({ class: "cm-draftly-line-h1" }),
51
+ "heading-2": Decoration.line({ class: "cm-draftly-line-h2" }),
52
+ "heading-3": Decoration.line({ class: "cm-draftly-line-h3" }),
53
+ "heading-4": Decoration.line({ class: "cm-draftly-line-h4" }),
54
+ "heading-5": Decoration.line({ class: "cm-draftly-line-h5" }),
55
+ "heading-6": Decoration.line({ class: "cm-draftly-line-h6" })
56
+ };
57
+ var HeadingPlugin = class extends DecorationPlugin {
58
+ name = "heading";
59
+ version = "1.0.0";
60
+ decorationPriority = 10;
61
+ requiredNodes = [
62
+ "ATXHeading1",
63
+ "ATXHeading2",
64
+ "ATXHeading3",
65
+ "ATXHeading4",
66
+ "ATXHeading5",
67
+ "ATXHeading6",
68
+ "HeaderMark"
69
+ ];
70
+ /**
71
+ * Constructor - calls super constructor
72
+ */
73
+ constructor() {
74
+ super();
75
+ }
76
+ /**
77
+ * Plugin theme
78
+ */
79
+ get theme() {
80
+ return theme2;
81
+ }
82
+ /**
83
+ * Build heading decorations by iterating the syntax tree
84
+ */
85
+ buildDecorations(ctx) {
86
+ const { view, decorations } = ctx;
87
+ const tree = syntaxTree(view.state);
88
+ tree.iterate({
89
+ enter: (node) => {
90
+ const { from, to, name } = node;
91
+ if (!HEADING_TYPES.includes(name)) {
92
+ return;
93
+ }
94
+ const level = parseInt(name.slice(-1), 10);
95
+ const headingClass = `heading-${level}`;
96
+ const lineClass = `heading-${level}`;
97
+ const line = view.state.doc.lineAt(from);
98
+ decorations.push(headingLineDecorations[lineClass].range(line.from));
99
+ decorations.push(headingMarkDecorations[headingClass].range(from, to));
100
+ const headingMark = node.node.getChild("HeaderMark");
101
+ if (headingMark) {
102
+ const markEnd = Math.min(headingMark.to + 1, line.to);
103
+ const cursorInNode = ctx.selectionOverlapsRange(from, to);
104
+ if (!cursorInNode) {
105
+ decorations.push(headingMarkDecorations["heading-mark"].range(headingMark.from, markEnd));
106
+ } else {
107
+ decorations.push(headingMarkDecorations["header-mark-class"].range(headingMark.from, markEnd));
108
+ }
109
+ }
110
+ }
111
+ });
112
+ }
113
+ renderToHTML(node, children) {
114
+ if (node.name === "HeaderMark") {
115
+ return "";
116
+ }
117
+ if (!HEADING_TYPES.includes(node.name)) {
118
+ return null;
119
+ }
120
+ const level = parseInt(node.name.slice(-1), 10);
121
+ const lineClass = headingLineDecorations[`heading-${level}`].spec.class;
122
+ const headingClass = headingMarkDecorations[`heading-${level}`].spec.class;
123
+ return `<div class="${lineClass}">
124
+ <h${level} class="${headingClass}">${children}</h${level}>
125
+ </div>
126
+ `;
127
+ }
128
+ };
129
+ var theme2 = createTheme({
130
+ default: {
131
+ ".cm-draftly-h1": {
132
+ fontSize: "2em",
133
+ fontWeight: "bold",
134
+ fontFamily: "sans-serif",
135
+ textDecoration: "none"
136
+ },
137
+ ".cm-draftly-h2": {
138
+ fontSize: "1.75em",
139
+ fontWeight: "bold",
140
+ fontFamily: "sans-serif",
141
+ textDecoration: "none"
142
+ },
143
+ ".cm-draftly-h3": {
144
+ fontSize: "1.5em",
145
+ fontWeight: "bold",
146
+ fontFamily: "sans-serif",
147
+ textDecoration: "none"
148
+ },
149
+ ".cm-draftly-h4": {
150
+ fontSize: "1.25em",
151
+ fontWeight: "bold",
152
+ fontFamily: "sans-serif",
153
+ textDecoration: "none"
154
+ },
155
+ ".cm-draftly-h5": {
156
+ fontSize: "1em",
157
+ fontWeight: "bold",
158
+ fontFamily: "sans-serif",
159
+ textDecoration: "none"
160
+ },
161
+ ".cm-draftly-h6": {
162
+ fontSize: "0.75em",
163
+ fontWeight: "bold",
164
+ fontFamily: "sans-serif",
165
+ textDecoration: "none"
166
+ },
167
+ // Heading line styles
168
+ ".cm-draftly-line-h1": {
169
+ paddingTop: "1.5em",
170
+ paddingBottom: "0.5em"
171
+ },
172
+ ".cm-draftly-line-h2": {
173
+ paddingTop: "1.25em",
174
+ paddingBottom: "0.5em"
175
+ },
176
+ ".cm-draftly-line-h3, .cm-draftly-line-h4, .cm-draftly-line-h5, .cm-draftly-line-h6": {
177
+ paddingTop: "1em",
178
+ paddingBottom: "0.5em"
179
+ },
180
+ ".cm-draftly-header-mark": {
181
+ opacity: 0.5
182
+ }
183
+ }
184
+ });
185
+ var INLINE_TYPES = {
186
+ Emphasis: "emphasis",
187
+ StrongEmphasis: "strong",
188
+ Strikethrough: "strikethrough",
189
+ Subscript: "subscript",
190
+ Superscript: "superscript",
191
+ Highlight: "highlight"
192
+ };
193
+ var inlineMarkDecorations = {
194
+ emphasis: Decoration.mark({ class: "cm-draftly-emphasis" }),
195
+ strong: Decoration.mark({ class: "cm-draftly-strong" }),
196
+ strikethrough: Decoration.mark({ class: "cm-draftly-strikethrough" }),
197
+ subscript: Decoration.mark({ class: "cm-draftly-subscript" }),
198
+ superscript: Decoration.mark({ class: "cm-draftly-superscript" }),
199
+ highlight: Decoration.mark({ class: "cm-draftly-highlight" }),
200
+ // Markers (* _ ~~ ^ ~ ==)
201
+ "inline-mark": Decoration.replace({})
202
+ };
203
+ var EQUALS = 61;
204
+ var Punctuation = /[!"#$%&'()*+,\-./:;<=>?@\[\\\]^_`{|}~\xA1\u2010-\u2027]/;
205
+ try {
206
+ Punctuation = new RegExp("[\\p{S}|\\p{P}]", "u");
207
+ } catch {
208
+ }
209
+ var HighlightDelim = { resolve: "Highlight", mark: "HighlightMark" };
210
+ var highlightParser = {
211
+ name: "Highlight",
212
+ parse(cx, next, pos) {
213
+ if (next !== EQUALS || cx.char(pos + 1) !== EQUALS) return -1;
214
+ if (cx.char(pos + 2) === EQUALS) return -1;
215
+ const before = cx.slice(pos - 1, pos);
216
+ const after = cx.slice(pos + 2, pos + 3);
217
+ const sBefore = /\s|^$/.test(before), sAfter = /\s|^$/.test(after);
218
+ const pBefore = Punctuation.test(before), pAfter = Punctuation.test(after);
219
+ return cx.addDelimiter(
220
+ HighlightDelim,
221
+ pos,
222
+ pos + 2,
223
+ !sAfter && (!pAfter || sBefore || pBefore),
224
+ !sBefore && (!pBefore || sAfter || pAfter)
225
+ );
226
+ }
227
+ };
228
+ var InlinePlugin = class extends DecorationPlugin {
229
+ name = "inline";
230
+ version = "1.0.0";
231
+ decorationPriority = 20;
232
+ requiredNodes = [
233
+ "Emphasis",
234
+ "StrongEmphasis",
235
+ "Strikethrough",
236
+ "Subscript",
237
+ "Superscript",
238
+ "Highlight",
239
+ "EmphasisMark",
240
+ "StrikethroughMark",
241
+ "SubscriptMark",
242
+ "SuperscriptMark",
243
+ "HighlightMark"
244
+ ];
245
+ marks = [];
246
+ constructor() {
247
+ super();
248
+ for (const mark of Object.keys(INLINE_TYPES)) {
249
+ this.marks.push(...this.getMarkerNames(mark));
250
+ }
251
+ }
252
+ /**
253
+ * Plugin theme
254
+ */
255
+ get theme() {
256
+ return theme3;
257
+ }
258
+ /**
259
+ * Keyboard shortcuts for inline formatting
260
+ */
261
+ getKeymap() {
262
+ return [
263
+ {
264
+ key: "Mod-b",
265
+ run: toggleMarkdownStyle("**"),
266
+ preventDefault: true
267
+ },
268
+ {
269
+ key: "Mod-i",
270
+ run: toggleMarkdownStyle("*"),
271
+ preventDefault: true
272
+ },
273
+ {
274
+ key: "Mod-Shift-s",
275
+ run: toggleMarkdownStyle("~~"),
276
+ preventDefault: true
277
+ },
278
+ {
279
+ key: "Mod-,",
280
+ run: toggleMarkdownStyle("~"),
281
+ preventDefault: true
282
+ },
283
+ {
284
+ key: "Mod-.",
285
+ run: toggleMarkdownStyle("^"),
286
+ preventDefault: true
287
+ },
288
+ {
289
+ key: "Mod-Shift-h",
290
+ run: toggleMarkdownStyle("=="),
291
+ preventDefault: true
292
+ }
293
+ ];
294
+ }
295
+ /**
296
+ * Return markdown parser extensions for highlight syntax (==text==)
297
+ */
298
+ getMarkdownConfig() {
299
+ return {
300
+ defineNodes: [
301
+ { name: "Highlight", style: tags.emphasis },
302
+ { name: "HighlightMark", style: tags.processingInstruction }
303
+ ],
304
+ parseInline: [highlightParser]
305
+ };
306
+ }
307
+ /**
308
+ * Build inline decorations by iterating the syntax tree
309
+ */
310
+ buildDecorations(ctx) {
311
+ const { view, decorations } = ctx;
312
+ const tree = syntaxTree(view.state);
313
+ tree.iterate({
314
+ enter: (node) => {
315
+ const { from, to, name } = node;
316
+ const inlineType = INLINE_TYPES[name];
317
+ if (!inlineType) {
318
+ return;
319
+ }
320
+ decorations.push(inlineMarkDecorations[inlineType].range(from, to));
321
+ const cursorInNode = ctx.selectionOverlapsRange(from, to);
322
+ if (!cursorInNode) {
323
+ const markerNames = this.getMarkerNames(name);
324
+ for (const markerName of markerNames) {
325
+ const marks = node.node.getChildren(markerName);
326
+ for (const mark of marks) {
327
+ decorations.push(inlineMarkDecorations["inline-mark"].range(mark.from, mark.to));
328
+ }
329
+ }
330
+ }
331
+ }
332
+ });
333
+ }
334
+ /**
335
+ * Get the marker node names for a given inline type
336
+ */
337
+ getMarkerNames(nodeType) {
338
+ switch (nodeType) {
339
+ case "Emphasis":
340
+ case "StrongEmphasis":
341
+ return ["EmphasisMark"];
342
+ case "Strikethrough":
343
+ return ["StrikethroughMark"];
344
+ case "Subscript":
345
+ return ["SubscriptMark"];
346
+ case "Superscript":
347
+ return ["SuperscriptMark"];
348
+ case "Highlight":
349
+ return ["HighlightMark"];
350
+ default:
351
+ return [];
352
+ }
353
+ }
354
+ renderToHTML(node, children) {
355
+ if (this.marks.includes(node.name)) {
356
+ return "";
357
+ }
358
+ const inlineType = INLINE_TYPES[node.name];
359
+ if (!inlineType) {
360
+ return null;
361
+ }
362
+ const className = inlineMarkDecorations[inlineType].spec.class;
363
+ return `<span class="${className}">${children}</span>`;
364
+ }
365
+ };
366
+ var theme3 = createTheme({
367
+ default: {
368
+ // Emphasis (italic)
369
+ ".cm-draftly-emphasis": {
370
+ fontStyle: "italic"
371
+ },
372
+ // Strong (bold)
373
+ ".cm-draftly-strong": {
374
+ fontWeight: "bold"
375
+ },
376
+ // Strikethrough
377
+ ".cm-draftly-strikethrough": {
378
+ textDecoration: "line-through",
379
+ opacity: "0.7"
380
+ },
381
+ // Subscript
382
+ ".cm-draftly-subscript": {
383
+ fontSize: "0.75em",
384
+ verticalAlign: "sub"
385
+ },
386
+ // Superscript
387
+ ".cm-draftly-superscript": {
388
+ fontSize: "0.75em",
389
+ verticalAlign: "super"
390
+ },
391
+ // Highlight
392
+ ".cm-draftly-highlight": {
393
+ backgroundColor: "rgba(255, 213, 0, 0.35)",
394
+ borderRadius: "2px",
395
+ padding: "1px 2px"
396
+ }
397
+ }
398
+ });
399
+ var linkMarkDecorations = {
400
+ "link-text": Decoration.mark({ class: "cm-draftly-link-text" }),
401
+ "link-marker": Decoration.mark({ class: "cm-draftly-link-marker" }),
402
+ "link-url": Decoration.mark({ class: "cm-draftly-link-url" }),
403
+ "link-hidden": Decoration.mark({ class: "cm-draftly-link-hidden" })
404
+ };
405
+ function parseLinkMarkdown(content) {
406
+ const match = content.match(/^\[([^\]]*)\]\(([^"\s)]+)(?:\s+"([^"]*)")?\s*\)$/);
407
+ if (!match) return null;
408
+ const result = {
409
+ text: match[1] || "",
410
+ url: match[2]
411
+ };
412
+ if (match[3] !== void 0) {
413
+ result.title = match[3];
414
+ }
415
+ return result;
416
+ }
417
+ var LinkTooltipWidget = class extends WidgetType {
418
+ constructor(url, from, to) {
419
+ super();
420
+ this.url = url;
421
+ this.from = from;
422
+ this.to = to;
423
+ }
424
+ eq(other) {
425
+ return other.url === this.url && other.from === this.from && other.to === this.to;
426
+ }
427
+ toDOM(view) {
428
+ const wrapper = document.createElement("span");
429
+ wrapper.className = "cm-draftly-link-wrapper";
430
+ wrapper.style.cursor = "pointer";
431
+ const tooltip = document.createElement("span");
432
+ tooltip.className = "cm-draftly-link-tooltip";
433
+ tooltip.textContent = this.url;
434
+ wrapper.appendChild(tooltip);
435
+ wrapper.addEventListener("mouseenter", () => {
436
+ tooltip.classList.add("cm-draftly-link-tooltip-visible");
437
+ });
438
+ wrapper.addEventListener("mouseleave", () => {
439
+ tooltip.classList.remove("cm-draftly-link-tooltip-visible");
440
+ });
441
+ wrapper.addEventListener("click", (e) => {
442
+ if (e.ctrlKey || e.metaKey) {
443
+ e.preventDefault();
444
+ e.stopPropagation();
445
+ window.open(this.url, "_blank", "noopener,noreferrer");
446
+ } else {
447
+ e.preventDefault();
448
+ e.stopPropagation();
449
+ view.dispatch({
450
+ selection: { anchor: this.from, head: this.to },
451
+ scrollIntoView: true
452
+ });
453
+ view.focus();
454
+ }
455
+ });
456
+ return wrapper;
457
+ }
458
+ ignoreEvent(event) {
459
+ return event.type !== "click" && event.type !== "mouseenter" && event.type !== "mouseleave";
460
+ }
461
+ };
462
+ var LinkPlugin = class extends DecorationPlugin {
463
+ name = "link";
464
+ version = "1.0.0";
465
+ decorationPriority = 22;
466
+ requiredNodes = ["Link"];
467
+ constructor() {
468
+ super();
469
+ }
470
+ /**
471
+ * Plugin theme
472
+ */
473
+ get theme() {
474
+ return theme4;
475
+ }
476
+ /**
477
+ * Keyboard shortcuts for link formatting
478
+ */
479
+ getKeymap() {
480
+ return [
481
+ {
482
+ key: "Mod-k",
483
+ run: (view) => this.toggleLink(view),
484
+ preventDefault: true
485
+ }
486
+ ];
487
+ }
488
+ /**
489
+ * URL regex pattern
490
+ */
491
+ urlPattern = /^(https?:\/\/|www\.)[^\s]+$/i;
492
+ /**
493
+ * Toggle link on selection
494
+ * - If text is selected and is a URL: [](url) with cursor in brackets
495
+ * - If text is selected (not URL): [text]() with cursor in parentheses
496
+ * - If nothing selected: []() with cursor in brackets
497
+ * - If already a link: remove syntax, leave plain text
498
+ */
499
+ toggleLink(view) {
500
+ const { state } = view;
501
+ const { from, to, empty } = state.selection.main;
502
+ const selectedText = state.sliceDoc(from, to);
503
+ const linkMatch = selectedText.match(/^\[([^\]]*)\]\(([^)]*)\)$/);
504
+ if (linkMatch) {
505
+ const linkText = linkMatch[1] || "";
506
+ view.dispatch({
507
+ changes: { from, to, insert: linkText },
508
+ selection: { anchor: from, head: from + linkText.length }
509
+ });
510
+ return true;
511
+ }
512
+ const lineStart = state.doc.lineAt(from).from;
513
+ const lineEnd = state.doc.lineAt(to).to;
514
+ const lineText = state.sliceDoc(lineStart, lineEnd);
515
+ const linkRegex = /\[([^\]]*)\]\(([^)]*)\)/g;
516
+ let match;
517
+ while ((match = linkRegex.exec(lineText)) !== null) {
518
+ const matchFrom = lineStart + match.index;
519
+ const matchTo = matchFrom + match[0].length;
520
+ if (from >= matchFrom && to <= matchTo) {
521
+ const linkText = match[1] || "";
522
+ view.dispatch({
523
+ changes: { from: matchFrom, to: matchTo, insert: linkText },
524
+ selection: { anchor: matchFrom, head: matchFrom + linkText.length }
525
+ });
526
+ return true;
527
+ }
528
+ }
529
+ if (empty) {
530
+ view.dispatch({
531
+ changes: { from, insert: "[]()" },
532
+ selection: { anchor: from + 1 }
533
+ });
534
+ } else if (this.urlPattern.test(selectedText)) {
535
+ const newText = `[](${selectedText})`;
536
+ view.dispatch({
537
+ changes: { from, to, insert: newText },
538
+ selection: { anchor: from + 1 }
539
+ });
540
+ } else {
541
+ const newText = `[${selectedText}]()`;
542
+ view.dispatch({
543
+ changes: { from, to, insert: newText },
544
+ selection: { anchor: from + selectedText.length + 3 }
545
+ });
546
+ }
547
+ return true;
548
+ }
549
+ buildDecorations(ctx) {
550
+ const { view, decorations } = ctx;
551
+ const tree = syntaxTree(view.state);
552
+ tree.iterate({
553
+ enter: (node) => {
554
+ const { from, to, name } = node;
555
+ if (name === "Link") {
556
+ const content = view.state.sliceDoc(from, to);
557
+ const parsed = parseLinkMarkdown(content);
558
+ if (!parsed) return;
559
+ const cursorInRange = ctx.selectionOverlapsRange(from, to);
560
+ if (cursorInRange) {
561
+ this.decorateRawLink(node.node, decorations, view);
562
+ } else {
563
+ decorations.push(linkMarkDecorations["link-hidden"].range(from, to));
564
+ decorations.push(
565
+ Decoration.widget({
566
+ widget: new LinkTooltipWidget(parsed.url, from, to),
567
+ side: 1
568
+ }).range(to)
569
+ );
570
+ decorations.push(
571
+ Decoration.replace({
572
+ widget: new LinkTextWidget(parsed.text, parsed.url, from, to, parsed.title)
573
+ }).range(from, to)
574
+ );
575
+ }
576
+ }
577
+ }
578
+ });
579
+ }
580
+ /**
581
+ * Decorate raw link markdown when cursor is in range
582
+ */
583
+ decorateRawLink(node, decorations, view) {
584
+ const content = view.state.sliceDoc(node.from, node.to);
585
+ decorations.push(linkMarkDecorations["link-marker"].range(node.from, node.from + 1));
586
+ const bracketParen = content.indexOf("](");
587
+ if (bracketParen !== -1) {
588
+ if (bracketParen > 1) {
589
+ decorations.push(linkMarkDecorations["link-text"].range(node.from + 1, node.from + bracketParen));
590
+ }
591
+ decorations.push(
592
+ linkMarkDecorations["link-marker"].range(node.from + bracketParen, node.from + bracketParen + 2)
593
+ );
594
+ const urlChild = node.getChild("URL");
595
+ if (urlChild) {
596
+ decorations.push(linkMarkDecorations["link-url"].range(urlChild.from, urlChild.to));
597
+ }
598
+ decorations.push(linkMarkDecorations["link-marker"].range(node.to - 1, node.to));
599
+ }
600
+ }
601
+ /**
602
+ * Render link to HTML for preview mode
603
+ */
604
+ renderToHTML(node, _children, ctx) {
605
+ if (node.name !== "Link") return null;
606
+ const content = ctx.sliceDoc(node.from, node.to);
607
+ const parsed = parseLinkMarkdown(content);
608
+ if (!parsed) return null;
609
+ const textContent = ctx.sanitize(parsed.text);
610
+ const urlAttr = ctx.sanitize(parsed.url);
611
+ const titleAttr = parsed.title ? ` title="${ctx.sanitize(parsed.title)}"` : "";
612
+ return `<a class="cm-draftly-link" href="${urlAttr}"${titleAttr} target="_blank" rel="noopener noreferrer">${textContent}</a>`;
613
+ }
614
+ };
615
+ var LinkTextWidget = class extends WidgetType {
616
+ constructor(text, url, from, to, title) {
617
+ super();
618
+ this.text = text;
619
+ this.url = url;
620
+ this.from = from;
621
+ this.to = to;
622
+ this.title = title;
623
+ }
624
+ eq(other) {
625
+ return other.text === this.text && other.url === this.url && other.from === this.from && other.to === this.to && other.title === this.title;
626
+ }
627
+ toDOM(view) {
628
+ const span = document.createElement("span");
629
+ span.className = "cm-draftly-link-styled";
630
+ span.textContent = this.text;
631
+ span.style.cursor = "pointer";
632
+ if (this.title) {
633
+ span.title = this.title;
634
+ }
635
+ const tooltip = document.createElement("span");
636
+ tooltip.className = "cm-draftly-link-tooltip";
637
+ tooltip.textContent = this.url;
638
+ span.appendChild(tooltip);
639
+ span.addEventListener("mouseenter", () => {
640
+ tooltip.classList.add("cm-draftly-link-tooltip-visible");
641
+ });
642
+ span.addEventListener("mouseleave", () => {
643
+ tooltip.classList.remove("cm-draftly-link-tooltip-visible");
644
+ });
645
+ span.addEventListener("click", (e) => {
646
+ if (e.ctrlKey || e.metaKey) {
647
+ e.preventDefault();
648
+ e.stopPropagation();
649
+ window.open(this.url, "_blank", "noopener,noreferrer");
650
+ } else {
651
+ e.preventDefault();
652
+ e.stopPropagation();
653
+ view.dispatch({
654
+ selection: { anchor: this.from, head: this.to },
655
+ scrollIntoView: true
656
+ });
657
+ view.focus();
658
+ }
659
+ });
660
+ return span;
661
+ }
662
+ ignoreEvent(event) {
663
+ return event.type !== "click" && event.type !== "mouseenter" && event.type !== "mouseleave";
664
+ }
665
+ };
666
+ var theme4 = createTheme({
667
+ default: {
668
+ // Link text
669
+ ".cm-draftly-link-text": {
670
+ color: "#0366d6"
671
+ },
672
+ // Link markers ([ ] ( ))
673
+ ".cm-draftly-link-marker": {
674
+ color: "#6a737d",
675
+ fontFamily: "var(--font-jetbrains-mono, monospace)"
676
+ },
677
+ // URL in raw markdown
678
+ ".cm-draftly-link-url": {
679
+ color: "#6a737d",
680
+ fontStyle: "italic"
681
+ },
682
+ // Hidden markdown syntax
683
+ ".cm-draftly-link-hidden": {
684
+ display: "none"
685
+ },
686
+ // Styled link when cursor is not in range
687
+ ".cm-draftly-link-styled": {
688
+ color: "#0366d6",
689
+ textDecoration: "underline",
690
+ position: "relative",
691
+ cursor: "pointer"
692
+ },
693
+ ".cm-draftly-link-styled:hover": {
694
+ color: "#0056b3"
695
+ },
696
+ // Preview link styling
697
+ ".cm-draftly-link": {
698
+ color: "#0366d6",
699
+ textDecoration: "underline"
700
+ },
701
+ ".cm-draftly-link:hover": {
702
+ color: "#0056b3"
703
+ },
704
+ // Tooltip styling
705
+ ".cm-draftly-link-tooltip": {
706
+ display: "none",
707
+ position: "absolute",
708
+ bottom: "100%",
709
+ left: "50%",
710
+ transform: "translateX(-50%)",
711
+ backgroundColor: "#24292e",
712
+ color: "#ffffff",
713
+ padding: "4px 8px",
714
+ borderRadius: "4px",
715
+ fontSize: "12px",
716
+ whiteSpace: "nowrap",
717
+ zIndex: "1000",
718
+ pointerEvents: "none",
719
+ marginBottom: "4px",
720
+ maxWidth: "300px",
721
+ overflow: "hidden",
722
+ textOverflow: "ellipsis"
723
+ },
724
+ ".cm-draftly-link-tooltip-visible": {
725
+ display: "block"
726
+ }
727
+ },
728
+ dark: {
729
+ ".cm-draftly-link-text": {
730
+ color: "#58a6ff"
731
+ },
732
+ ".cm-draftly-link-marker": {
733
+ color: "#8b949e"
734
+ },
735
+ ".cm-draftly-link-url": {
736
+ color: "#8b949e"
737
+ },
738
+ ".cm-draftly-link-styled": {
739
+ color: "#58a6ff"
740
+ },
741
+ ".cm-draftly-link-styled:hover": {
742
+ color: "#79c0ff"
743
+ },
744
+ ".cm-draftly-link": {
745
+ color: "#58a6ff"
746
+ },
747
+ ".cm-draftly-link:hover": {
748
+ color: "#79c0ff"
749
+ },
750
+ ".cm-draftly-link-tooltip": {
751
+ backgroundColor: "#30363d",
752
+ color: "#c9d1d9"
753
+ }
754
+ }
755
+ });
756
+ var classes = {
757
+ // Unordered list classes
758
+ lineUL: "cm-draftly-list-line-ul",
759
+ markUL: "cm-draftly-list-mark-ul",
760
+ // Ordered list classes
761
+ lineOL: "cm-draftly-list-line-ol",
762
+ markOL: "cm-draftly-list-mark-ol",
763
+ // Task list classes
764
+ taskLine: "cm-draftly-task-line",
765
+ taskMarker: "cm-draftly-task-marker",
766
+ // Common classes
767
+ content: "cm-draftly-list-content",
768
+ indent: "cm-draftly-list-indent",
769
+ active: " cm-draftly-active",
770
+ preview: "cm-draftly-preview"
771
+ };
772
+ var TaskCheckboxWidget = class extends WidgetType {
773
+ constructor(checked) {
774
+ super();
775
+ this.checked = checked;
776
+ }
777
+ eq(other) {
778
+ return other.checked === this.checked;
779
+ }
780
+ toDOM(view) {
781
+ const wrap = document.createElement("span");
782
+ wrap.className = `cm-draftly-task-checkbox ${this.checked ? "checked" : ""}`;
783
+ wrap.setAttribute("aria-hidden", "true");
784
+ const checkbox = document.createElement("input");
785
+ checkbox.type = "checkbox";
786
+ checkbox.checked = this.checked;
787
+ checkbox.tabIndex = -1;
788
+ checkbox.addEventListener("mousedown", (e) => {
789
+ e.preventDefault();
790
+ this.toggleCheckbox(view, wrap);
791
+ });
792
+ wrap.appendChild(checkbox);
793
+ return wrap;
794
+ }
795
+ ignoreEvent() {
796
+ return false;
797
+ }
798
+ /** Toggle the checkbox state in the document */
799
+ toggleCheckbox(view, wrap) {
800
+ const pos = view.posAtDOM(wrap);
801
+ const line = view.state.doc.lineAt(pos);
802
+ const match = line.text.match(/^(\s*(?:[-*+]|\d+\.)\s*)\[([ xX])\]/);
803
+ if (match) {
804
+ const markerStart = line.from + match[1].length + 1;
805
+ const newChar = this.checked ? " " : "x";
806
+ view.dispatch({
807
+ changes: { from: markerStart, to: markerStart + 1, insert: newChar }
808
+ });
809
+ }
810
+ }
811
+ };
812
+ var ListPlugin = class extends DecorationPlugin {
813
+ name = "list";
814
+ version = "1.0.0";
815
+ decorationPriority = 20;
816
+ requiredNodes = [
817
+ "BulletList",
818
+ "OrderedList",
819
+ "ListItem",
820
+ "ListMark",
821
+ "Task",
822
+ "TaskMarker"
823
+ ];
824
+ get theme() {
825
+ return theme5;
826
+ }
827
+ /**
828
+ * Keyboard shortcuts for list formatting
829
+ */
830
+ getKeymap() {
831
+ return [
832
+ {
833
+ key: "Mod-Shift-8",
834
+ run: (view) => this.toggleListOnLines(view, "- "),
835
+ preventDefault: true
836
+ },
837
+ {
838
+ key: "Mod-Shift-7",
839
+ run: (view) => this.toggleListOnLines(view, "1. "),
840
+ preventDefault: true
841
+ },
842
+ {
843
+ key: "Mod-Shift-9",
844
+ run: (view) => this.toggleListOnLines(view, "- [ ] "),
845
+ preventDefault: true
846
+ }
847
+ ];
848
+ }
849
+ /**
850
+ * Toggle list marker on current line or selected lines
851
+ */
852
+ toggleListOnLines(view, marker) {
853
+ const { state } = view;
854
+ const { from, to } = state.selection.main;
855
+ const startLine = state.doc.lineAt(from);
856
+ const endLine = state.doc.lineAt(to);
857
+ const changes = [];
858
+ const listMarkerRegex = /^(\s*)([-*+]|\d+\.)\s(\[[ xX]\]\s)?/;
859
+ const isOrderedMarker = marker === "1. ";
860
+ let orderNum = 1;
861
+ for (let lineNum = startLine.number; lineNum <= endLine.number; lineNum++) {
862
+ const line = state.doc.line(lineNum);
863
+ const match = line.text.match(listMarkerRegex);
864
+ const actualMarker = isOrderedMarker ? `${orderNum}. ` : marker;
865
+ if (match) {
866
+ const existingMarker = match[0];
867
+ const indent = match[1] || "";
868
+ const isUnordered = /^[-*+]$/.test(match[2]);
869
+ const isOrdered = /^\d+\.$/.test(match[2]);
870
+ const hasTask = !!match[3];
871
+ const wantUnordered = marker === "- ";
872
+ const wantOrdered = isOrderedMarker;
873
+ const wantTask = marker === "- [ ] ";
874
+ if (wantUnordered && isUnordered && !hasTask || wantOrdered && isOrdered && !hasTask || wantTask && hasTask) {
875
+ changes.push({
876
+ from: line.from,
877
+ to: line.from + existingMarker.length,
878
+ insert: indent
879
+ });
880
+ } else {
881
+ changes.push({
882
+ from: line.from,
883
+ to: line.from + existingMarker.length,
884
+ insert: indent + actualMarker
885
+ });
886
+ orderNum++;
887
+ }
888
+ } else {
889
+ const indentMatch = line.text.match(/^(\s*)/);
890
+ const indent = indentMatch ? indentMatch[1] : "";
891
+ changes.push({
892
+ from: line.from + indent.length,
893
+ to: line.from + indent.length,
894
+ insert: actualMarker
895
+ });
896
+ orderNum++;
897
+ }
898
+ }
899
+ if (changes.length > 0) {
900
+ view.dispatch({ changes });
901
+ }
902
+ return true;
903
+ }
904
+ buildDecorations(ctx) {
905
+ const { view, decorations } = ctx;
906
+ const tree = syntaxTree(view.state);
907
+ tree.iterate({
908
+ enter: (node) => {
909
+ const { from, to, name } = node;
910
+ const line = view.state.doc.lineAt(from);
911
+ const cursorInLine = ctx.cursorInRange(line.from, line.to);
912
+ switch (name) {
913
+ case "ListItem":
914
+ this.decorateListItem(node, line, decorations);
915
+ break;
916
+ case "ListMark":
917
+ this.decorateListMark(node, line, decorations, cursorInLine);
918
+ break;
919
+ case "TaskMarker":
920
+ this.decorateTaskMarker(from, to, view, decorations, cursorInLine);
921
+ break;
922
+ }
923
+ }
924
+ });
925
+ }
926
+ /** Add line decoration for list items with nesting depth */
927
+ decorateListItem(node, line, decorations) {
928
+ const parent = node.node.parent;
929
+ const listType = parent?.name;
930
+ let depth = 0;
931
+ let ancestor = node.node.parent;
932
+ while (ancestor) {
933
+ if (ancestor.name === "ListItem") depth++;
934
+ ancestor = ancestor.parent;
935
+ }
936
+ const hasTask = this.hasTaskChild(node);
937
+ let lineClass;
938
+ if (hasTask) lineClass = classes.taskLine;
939
+ else if (listType === "OrderedList") lineClass = classes.lineOL;
940
+ else lineClass = classes.lineUL;
941
+ decorations.push(
942
+ Decoration.line({
943
+ class: lineClass,
944
+ attributes: { style: `--depth: ${depth}` }
945
+ }).range(line.from)
946
+ );
947
+ }
948
+ /** Check if a ListItem node has a Task child */
949
+ hasTaskChild(node) {
950
+ const cursor = node.node.cursor();
951
+ if (cursor.firstChild()) {
952
+ do {
953
+ if (cursor.name === "Task") return true;
954
+ } while (cursor.nextSibling());
955
+ }
956
+ return false;
957
+ }
958
+ /** Decorate list markers (bullets for UL, numbers for OL) */
959
+ decorateListMark(node, line, decorations, cursorInLine) {
960
+ const { from, to } = node;
961
+ const parent = node.node.parent;
962
+ const grandparent = parent?.parent;
963
+ const listType = grandparent?.name;
964
+ const activeClass = cursorInLine ? classes.active : "";
965
+ if (from > line.from) {
966
+ decorations.push(Decoration.mark({ class: classes.indent + activeClass }).range(line.from, from));
967
+ }
968
+ const markClass = listType === "OrderedList" ? classes.markOL : classes.markUL;
969
+ decorations.push(Decoration.mark({ class: markClass + activeClass }).range(from, to + 1));
970
+ const contentStart = to + 1;
971
+ if (contentStart < line.to) {
972
+ decorations.push(Decoration.mark({ class: classes.content }).range(contentStart, line.to));
973
+ }
974
+ }
975
+ /** Decorate task markers - show checkbox widget or raw text based on cursor */
976
+ decorateTaskMarker(from, to, view, decorations, cursorInLine) {
977
+ const text = view.state.sliceDoc(from, to);
978
+ const isChecked = text.includes("x") || text.includes("X");
979
+ if (cursorInLine) {
980
+ decorations.push(Decoration.mark({ class: classes.taskMarker }).range(from, to));
981
+ } else {
982
+ decorations.push(
983
+ Decoration.replace({
984
+ widget: new TaskCheckboxWidget(isChecked)
985
+ }).range(from, to)
986
+ );
987
+ }
988
+ }
989
+ /** Render list nodes to HTML */
990
+ renderToHTML(node, children, ctx) {
991
+ switch (node.name) {
992
+ case "BulletList":
993
+ return `<ul class="${classes.lineUL} ${classes.preview}">${children}</ul>
994
+ `;
995
+ case "OrderedList":
996
+ return `<ol class="${classes.lineOL} ${classes.preview}">${children}</ol>
997
+ `;
998
+ case "ListItem":
999
+ return `<li>${children}</li>
1000
+ `;
1001
+ case "Task":
1002
+ return children;
1003
+ case "TaskMarker": {
1004
+ const text = ctx.sliceDoc(node.from, node.to);
1005
+ const isChecked = text.includes("x") || text.includes("X");
1006
+ return `<input type="checkbox" class="cm-draftly-task-checkbox" disabled ${isChecked ? "checked" : ""} />`;
1007
+ }
1008
+ case "ListMark":
1009
+ return "";
1010
+ default:
1011
+ return null;
1012
+ }
1013
+ }
1014
+ };
1015
+ var theme5 = createTheme({
1016
+ default: {
1017
+ // Indentation marker positioning
1018
+ ".cm-draftly-list-indent": {
1019
+ overflow: "hidden",
1020
+ display: "inline-block",
1021
+ position: "absolute",
1022
+ left: "calc(1rem * (var(--depth, 0) + 1))",
1023
+ transform: "translateX(-100%)"
1024
+ },
1025
+ // List line layout (flexbox for marker alignment)
1026
+ ".cm-draftly-list-line-ul, .cm-draftly-list-line-ol": {
1027
+ position: "relative",
1028
+ paddingLeft: "calc(1rem * (var(--depth, 0) + 1)) !important",
1029
+ display: "flex",
1030
+ alignItems: "start"
1031
+ },
1032
+ ".cm-draftly-list-line-ul > :first-child, .cm-draftly-list-line-ol > :first-child": {
1033
+ flexShrink: 0
1034
+ },
1035
+ // List marker sizing
1036
+ ".cm-draftly-list-line-ul .cm-draftly-list-mark-ul, .cm-draftly-list-line-ol .cm-draftly-list-mark-ol": {
1037
+ whiteSpace: "pre",
1038
+ position: "relative",
1039
+ width: "1rem",
1040
+ flexShrink: 0
1041
+ },
1042
+ // Hide raw marker text when not active
1043
+ ".cm-draftly-list-mark-ul:not(.cm-draftly-active) > span, .cm-draftly-task-line .cm-draftly-list-mark-ol:not(.cm-draftly-active) > span": {
1044
+ visibility: "hidden",
1045
+ display: "none"
1046
+ },
1047
+ // Styled bullet for unordered lists
1048
+ ".cm-draftly-list-line-ul .cm-draftly-list-mark-ul:not(.cm-draftly-active)::after": {
1049
+ content: '"\u2022"',
1050
+ color: "var(--color-link)",
1051
+ fontWeight: "bold",
1052
+ pointerEvents: "none"
1053
+ },
1054
+ // Task marker styling (visible when editing)
1055
+ ".cm-draftly-task-marker": {
1056
+ color: "var(--draftly-highlight, #a4a4a4)",
1057
+ fontFamily: "monospace"
1058
+ },
1059
+ // Task checkbox container
1060
+ ".cm-draftly-task-checkbox": {
1061
+ display: "inline-flex",
1062
+ verticalAlign: "middle",
1063
+ marginRight: "0.3em",
1064
+ cursor: "pointer",
1065
+ userSelect: "none",
1066
+ alignItems: "center",
1067
+ height: "1.2em"
1068
+ },
1069
+ // Task checkbox input styling
1070
+ ".cm-draftly-task-checkbox input": {
1071
+ cursor: "pointer",
1072
+ margin: 0,
1073
+ width: "1.1em",
1074
+ height: "1.1em",
1075
+ appearance: "none",
1076
+ border: "1px solid",
1077
+ borderRadius: "0.25em",
1078
+ backgroundColor: "transparent",
1079
+ position: "relative"
1080
+ },
1081
+ // Checkmark for completed tasks
1082
+ ".cm-draftly-task-checkbox.checked input::after": {
1083
+ content: '"\u2713"',
1084
+ position: "absolute",
1085
+ left: "1px",
1086
+ top: "-3px"
1087
+ },
1088
+ // Preview styles (override editor-specific layout)
1089
+ ".cm-draftly-preview": {
1090
+ display: "block",
1091
+ paddingLeft: "1.5rem",
1092
+ margin: "0.5rem 0"
1093
+ },
1094
+ ".cm-draftly-preview li": {
1095
+ display: "list-item",
1096
+ marginBottom: "0.25rem"
1097
+ },
1098
+ "ul.cm-draftly-preview": {
1099
+ listStyleType: "disc"
1100
+ },
1101
+ "ol.cm-draftly-preview": {
1102
+ listStyleType: "decimal"
1103
+ },
1104
+ // Hide list marker for task items
1105
+ ".cm-draftly-preview li:has(.cm-draftly-task-checkbox)": {
1106
+ listStyleType: "none"
1107
+ },
1108
+ ".cm-draftly-preview li .cm-draftly-paragraph": {
1109
+ padding: "0"
1110
+ }
1111
+ }
1112
+ });
1113
+ var htmlMarkDecorations = {
1114
+ "html-tag": Decoration.mark({ class: "cm-draftly-html-tag" }),
1115
+ "html-comment": Decoration.mark({ class: "cm-draftly-html-comment" })
1116
+ };
1117
+ var htmlLineDecorations = {
1118
+ "html-block": Decoration.line({ class: "cm-draftly-line-html-block" }),
1119
+ "hidden-line": Decoration.line({ class: "cm-draftly-hidden-line" })
1120
+ };
1121
+ var HTMLPreviewWidget = class extends WidgetType {
1122
+ constructor(html) {
1123
+ super();
1124
+ this.html = html;
1125
+ }
1126
+ eq(other) {
1127
+ return other.html === this.html;
1128
+ }
1129
+ toDOM() {
1130
+ const div = document.createElement("div");
1131
+ div.className = "cm-draftly-html-preview";
1132
+ div.innerHTML = DOMPurify.sanitize(this.html);
1133
+ return div;
1134
+ }
1135
+ ignoreEvent() {
1136
+ return false;
1137
+ }
1138
+ };
1139
+ var InlineHTMLPreviewWidget = class extends WidgetType {
1140
+ constructor(html) {
1141
+ super();
1142
+ this.html = html;
1143
+ }
1144
+ eq(other) {
1145
+ return other.html === this.html;
1146
+ }
1147
+ toDOM() {
1148
+ const span = document.createElement("span");
1149
+ span.className = "cm-draftly-inline-html-preview";
1150
+ span.innerHTML = DOMPurify.sanitize(this.html);
1151
+ return span;
1152
+ }
1153
+ ignoreEvent() {
1154
+ return false;
1155
+ }
1156
+ };
1157
+ function parseHTMLTag(content) {
1158
+ const match = content.match(/^<\s*(\/?)([a-zA-Z][a-zA-Z0-9-]*)[^>]*(\/?)>$/);
1159
+ if (!match) return null;
1160
+ return {
1161
+ tagName: match[2].toLowerCase(),
1162
+ isClosing: match[1] === "/",
1163
+ isSelfClosing: match[3] === "/" || ["br", "hr", "img", "input", "meta", "link", "area", "base", "col", "embed", "source", "track", "wbr"].includes(
1164
+ match[2].toLowerCase()
1165
+ )
1166
+ };
1167
+ }
1168
+ var HTMLPlugin = class extends DecorationPlugin {
1169
+ name = "html";
1170
+ version = "1.0.0";
1171
+ decorationPriority = 30;
1172
+ constructor() {
1173
+ super();
1174
+ }
1175
+ /**
1176
+ * Plugin theme
1177
+ */
1178
+ get theme() {
1179
+ return theme6;
1180
+ }
1181
+ buildDecorations(ctx) {
1182
+ const { view, decorations } = ctx;
1183
+ const tree = syntaxTree(view.state);
1184
+ const htmlGroups = [];
1185
+ const htmlTags = [];
1186
+ tree.iterate({
1187
+ enter: (node) => {
1188
+ const { from, to, name } = node;
1189
+ if (name === "Comment") {
1190
+ decorations.push(htmlMarkDecorations["html-comment"].range(from, to));
1191
+ return;
1192
+ }
1193
+ if (name === "HTMLTag") {
1194
+ const content = view.state.sliceDoc(from, to);
1195
+ const parsed = parseHTMLTag(content);
1196
+ if (parsed) {
1197
+ htmlTags.push({
1198
+ from,
1199
+ to,
1200
+ tagName: parsed.tagName,
1201
+ isClosing: parsed.isClosing,
1202
+ isSelfClosing: parsed.isSelfClosing
1203
+ });
1204
+ }
1205
+ }
1206
+ if (name === "HTMLBlock") {
1207
+ const last = htmlGroups[htmlGroups.length - 1];
1208
+ if (last) {
1209
+ const gap = view.state.sliceDoc(last.to, from);
1210
+ if (!gap.trim()) {
1211
+ last.to = to;
1212
+ return;
1213
+ }
1214
+ }
1215
+ htmlGroups.push({ from, to });
1216
+ }
1217
+ }
1218
+ });
1219
+ const inlineElements = [];
1220
+ const usedTags = /* @__PURE__ */ new Set();
1221
+ for (let i = 0; i < htmlTags.length; i++) {
1222
+ if (usedTags.has(i)) continue;
1223
+ const openTag = htmlTags[i];
1224
+ if (openTag.isClosing) continue;
1225
+ if (openTag.isSelfClosing) {
1226
+ inlineElements.push({
1227
+ from: openTag.from,
1228
+ to: openTag.to,
1229
+ content: view.state.sliceDoc(openTag.from, openTag.to)
1230
+ });
1231
+ usedTags.add(i);
1232
+ continue;
1233
+ }
1234
+ const openLine = view.state.doc.lineAt(openTag.from);
1235
+ let depth = 1;
1236
+ let closeTagIndex = null;
1237
+ for (let j = i + 1; j < htmlTags.length && depth > 0; j++) {
1238
+ const tag = htmlTags[j];
1239
+ if (tag.from > openLine.to) break;
1240
+ if (tag.tagName === openTag.tagName) {
1241
+ if (tag.isClosing) {
1242
+ depth--;
1243
+ if (depth === 0) {
1244
+ closeTagIndex = j;
1245
+ }
1246
+ } else if (!tag.isSelfClosing) {
1247
+ depth++;
1248
+ }
1249
+ }
1250
+ }
1251
+ if (closeTagIndex !== null) {
1252
+ const closeTag = htmlTags[closeTagIndex];
1253
+ inlineElements.push({
1254
+ from: openTag.from,
1255
+ to: closeTag.to,
1256
+ content: view.state.sliceDoc(openTag.from, closeTag.to)
1257
+ });
1258
+ for (let k = i; k <= closeTagIndex; k++) {
1259
+ usedTags.add(k);
1260
+ }
1261
+ }
1262
+ }
1263
+ inlineElements.sort((a, b) => a.from - b.from);
1264
+ const filteredElements = [];
1265
+ let lastEnd = -1;
1266
+ for (const elem of inlineElements) {
1267
+ if (elem.from >= lastEnd) {
1268
+ filteredElements.push(elem);
1269
+ lastEnd = elem.to;
1270
+ }
1271
+ }
1272
+ for (const elem of filteredElements) {
1273
+ const cursorInRange = ctx.cursorInRange(elem.from, elem.to);
1274
+ if (cursorInRange) {
1275
+ for (const tag of htmlTags) {
1276
+ if (tag.from >= elem.from && tag.to <= elem.to) {
1277
+ decorations.push(htmlMarkDecorations["html-tag"].range(tag.from, tag.to));
1278
+ }
1279
+ }
1280
+ } else {
1281
+ decorations.push(
1282
+ Decoration.replace({
1283
+ widget: new InlineHTMLPreviewWidget(elem.content)
1284
+ }).range(elem.from, elem.to)
1285
+ );
1286
+ }
1287
+ }
1288
+ for (let i = 0; i < htmlTags.length; i++) {
1289
+ if (!usedTags.has(i)) {
1290
+ const tag = htmlTags[i];
1291
+ decorations.push(htmlMarkDecorations["html-tag"].range(tag.from, tag.to));
1292
+ }
1293
+ }
1294
+ for (const group of htmlGroups) {
1295
+ const { from, to } = group;
1296
+ const nodeLineStart = view.state.doc.lineAt(from);
1297
+ const nodeLineEnd = view.state.doc.lineAt(to);
1298
+ const cursorInRange = ctx.cursorInRange(nodeLineStart.from, nodeLineEnd.to);
1299
+ if (cursorInRange) {
1300
+ for (let i = nodeLineStart.number; i <= nodeLineEnd.number; i++) {
1301
+ const line = view.state.doc.line(i);
1302
+ decorations.push(htmlLineDecorations["html-block"].range(line.from));
1303
+ }
1304
+ } else {
1305
+ const htmlContent = view.state.sliceDoc(from, to);
1306
+ decorations.push(
1307
+ Decoration.replace({
1308
+ widget: new HTMLPreviewWidget(htmlContent.trim())
1309
+ }).range(from, nodeLineStart.to)
1310
+ );
1311
+ for (let i = nodeLineStart.number + 1; i <= nodeLineEnd.number; i++) {
1312
+ const line = view.state.doc.line(i);
1313
+ decorations.push(htmlLineDecorations["hidden-line"].range(line.from));
1314
+ }
1315
+ }
1316
+ }
1317
+ }
1318
+ };
1319
+ var theme6 = createTheme({
1320
+ default: {
1321
+ ".cm-draftly-html-tag": {
1322
+ color: "#6a737d",
1323
+ fontFamily: "var(--font-jetbrains-mono, monospace)",
1324
+ fontSize: "0.85em"
1325
+ },
1326
+ ".cm-draftly-html-comment": {
1327
+ color: "#6a737d",
1328
+ fontStyle: "italic",
1329
+ fontFamily: "var(--font-jetbrains-mono, monospace)",
1330
+ fontSize: "0.85em",
1331
+ opacity: 0.5
1332
+ },
1333
+ ".cm-draftly-line-html-block": {
1334
+ backgroundColor: "rgba(0, 0, 0, 0.02)"
1335
+ },
1336
+ ".cm-draftly-hidden-line": {
1337
+ display: "none"
1338
+ },
1339
+ ".cm-draftly-html-preview": {
1340
+ display: "inline-block",
1341
+ width: "100%",
1342
+ verticalAlign: "top",
1343
+ margin: "0",
1344
+ whiteSpace: "normal",
1345
+ lineHeight: "1.4"
1346
+ },
1347
+ ".cm-draftly-html-preview > *:first-child": {
1348
+ marginTop: "0"
1349
+ },
1350
+ ".cm-draftly-html-preview > *:last-child": {
1351
+ marginBottom: "0"
1352
+ },
1353
+ ".cm-draftly-inline-html-preview": {
1354
+ display: "inline",
1355
+ whiteSpace: "normal"
1356
+ }
1357
+ }
1358
+ });
1359
+ var imageMarkDecorations = {
1360
+ "image-block": Decoration.line({ class: "cm-draftly-image-block" }),
1361
+ "image-marker": Decoration.mark({ class: "cm-draftly-image-marker" }),
1362
+ "image-alt": Decoration.mark({ class: "cm-draftly-image-alt" }),
1363
+ "image-url": Decoration.mark({ class: "cm-draftly-image-url" }),
1364
+ "image-hidden": Decoration.mark({ class: "cm-draftly-image-hidden" })
1365
+ };
1366
+ function parseImageMarkdown(content) {
1367
+ const match = content.match(/^!\[([^\]]*)\]\(([^"\s)]+)(?:\s+"([^"]*)")?\s*\)$/);
1368
+ if (!match) return null;
1369
+ const result = {
1370
+ alt: match[1] || "",
1371
+ url: match[2]
1372
+ };
1373
+ if (match[3] !== void 0) {
1374
+ result.title = match[3];
1375
+ }
1376
+ return result;
1377
+ }
1378
+ var ImageWidget = class extends WidgetType {
1379
+ constructor(url, alt, from, to, title) {
1380
+ super();
1381
+ this.url = url;
1382
+ this.alt = alt;
1383
+ this.from = from;
1384
+ this.to = to;
1385
+ this.title = title;
1386
+ }
1387
+ eq(other) {
1388
+ return other.url === this.url && other.alt === this.alt && other.from === this.from && other.to === this.to && other.title === this.title;
1389
+ }
1390
+ toDOM(view) {
1391
+ const figure = document.createElement("figure");
1392
+ figure.className = "cm-draftly-image-figure";
1393
+ figure.setAttribute("role", "figure");
1394
+ figure.style.cursor = "pointer";
1395
+ if (this.title) {
1396
+ figure.setAttribute("aria-label", this.title);
1397
+ }
1398
+ figure.addEventListener("click", (e) => {
1399
+ e.preventDefault();
1400
+ e.stopPropagation();
1401
+ view.dispatch({
1402
+ selection: { anchor: this.from, head: this.to },
1403
+ scrollIntoView: true
1404
+ });
1405
+ view.focus();
1406
+ });
1407
+ const img = document.createElement("img");
1408
+ img.className = "cm-draftly-image";
1409
+ img.src = this.url;
1410
+ img.alt = this.alt;
1411
+ img.setAttribute("loading", "lazy");
1412
+ img.setAttribute("decoding", "async");
1413
+ if (this.title) {
1414
+ img.title = this.title;
1415
+ }
1416
+ img.onerror = () => {
1417
+ img.style.display = "none";
1418
+ const errorSpan = document.createElement("span");
1419
+ errorSpan.className = "cm-draftly-image-error";
1420
+ errorSpan.setAttribute("role", "alert");
1421
+ errorSpan.textContent = `[Image not found: ${this.alt || this.url}]`;
1422
+ figure.appendChild(errorSpan);
1423
+ };
1424
+ figure.appendChild(img);
1425
+ if (this.title) {
1426
+ const figcaption = document.createElement("figcaption");
1427
+ figcaption.className = "cm-draftly-image-caption";
1428
+ figcaption.textContent = this.title;
1429
+ figure.appendChild(figcaption);
1430
+ }
1431
+ return figure;
1432
+ }
1433
+ ignoreEvent(event) {
1434
+ return event.type !== "click";
1435
+ }
1436
+ };
1437
+ var ImagePlugin = class extends DecorationPlugin {
1438
+ name = "image";
1439
+ version = "1.0.0";
1440
+ decorationPriority = 25;
1441
+ requiredNodes = ["Image"];
1442
+ constructor() {
1443
+ super();
1444
+ }
1445
+ /**
1446
+ * Plugin theme
1447
+ */
1448
+ get theme() {
1449
+ return theme7;
1450
+ }
1451
+ /**
1452
+ * Keyboard shortcuts for image formatting
1453
+ */
1454
+ getKeymap() {
1455
+ return [
1456
+ {
1457
+ key: "Mod-Shift-i",
1458
+ run: (view) => this.toggleImage(view),
1459
+ preventDefault: true
1460
+ }
1461
+ ];
1462
+ }
1463
+ /**
1464
+ * URL regex pattern
1465
+ */
1466
+ urlPattern = /^(https?:\/\/|www\.)[^\s]+$/i;
1467
+ /**
1468
+ * Toggle image on selection
1469
+ * - If text selected and is a URL: ![Alt Text](url) with cursor in brackets
1470
+ * - If text selected (not URL): ![text]() with cursor in parentheses
1471
+ * - If nothing selected: ![Alt Text]() with cursor in parentheses
1472
+ * - If already an image: remove syntax, leave just the URL
1473
+ */
1474
+ toggleImage(view) {
1475
+ const { state } = view;
1476
+ const { from, to, empty } = state.selection.main;
1477
+ const selectedText = state.sliceDoc(from, to);
1478
+ const imageMatch = selectedText.match(/^!\[([^\]]*)\]\(([^)]*)\)$/);
1479
+ if (imageMatch) {
1480
+ const imageUrl = imageMatch[2] || "";
1481
+ view.dispatch({
1482
+ changes: { from, to, insert: imageUrl },
1483
+ selection: { anchor: from, head: from + imageUrl.length }
1484
+ });
1485
+ return true;
1486
+ }
1487
+ const lineStart = state.doc.lineAt(from).from;
1488
+ const lineEnd = state.doc.lineAt(to).to;
1489
+ const lineText = state.sliceDoc(lineStart, lineEnd);
1490
+ const imageRegex = /!\[([^\]]*)\]\(([^)]*)\)/g;
1491
+ let match;
1492
+ while ((match = imageRegex.exec(lineText)) !== null) {
1493
+ const matchFrom = lineStart + match.index;
1494
+ const matchTo = matchFrom + match[0].length;
1495
+ if (from >= matchFrom && to <= matchTo) {
1496
+ const imageUrl = match[2] || "";
1497
+ view.dispatch({
1498
+ changes: { from: matchFrom, to: matchTo, insert: imageUrl },
1499
+ selection: { anchor: matchFrom, head: matchFrom + imageUrl.length }
1500
+ });
1501
+ return true;
1502
+ }
1503
+ }
1504
+ if (empty) {
1505
+ const defaultAlt = "Alt Text";
1506
+ const newText = `![${defaultAlt}]()`;
1507
+ view.dispatch({
1508
+ changes: { from, insert: newText },
1509
+ selection: { anchor: from + defaultAlt.length + 4 }
1510
+ // After ![Alt Text](
1511
+ });
1512
+ } else if (this.urlPattern.test(selectedText)) {
1513
+ const defaultAlt = "Alt Text";
1514
+ const newText = `![${defaultAlt}](${selectedText})`;
1515
+ view.dispatch({
1516
+ changes: { from, to, insert: newText },
1517
+ selection: { anchor: from + 2, head: from + 2 + defaultAlt.length }
1518
+ // Select "Alt Text"
1519
+ });
1520
+ } else {
1521
+ const newText = `![${selectedText}]()`;
1522
+ view.dispatch({
1523
+ changes: { from, to, insert: newText },
1524
+ selection: { anchor: from + selectedText.length + 4 }
1525
+ // After ![text](
1526
+ });
1527
+ }
1528
+ return true;
1529
+ }
1530
+ buildDecorations(ctx) {
1531
+ const { view, decorations } = ctx;
1532
+ const tree = syntaxTree(view.state);
1533
+ tree.iterate({
1534
+ enter: (node) => {
1535
+ const { from, to, name } = node;
1536
+ if (name === "Image") {
1537
+ const content = view.state.sliceDoc(from, to);
1538
+ const parsed = parseImageMarkdown(content);
1539
+ if (!parsed) return;
1540
+ const cursorInRange = ctx.selectionOverlapsRange(from, to);
1541
+ decorations.push(imageMarkDecorations["image-block"].range(from));
1542
+ decorations.push(
1543
+ Decoration.widget({
1544
+ widget: new ImageWidget(parsed.url, parsed.alt, from, to, parsed.title),
1545
+ side: 1,
1546
+ // Place after the position
1547
+ block: false
1548
+ // Don't create a new line
1549
+ }).range(to)
1550
+ );
1551
+ if (cursorInRange) {
1552
+ this.decorateRawImage(node.node, decorations, view);
1553
+ } else {
1554
+ decorations.push(imageMarkDecorations["image-hidden"].range(from, to));
1555
+ }
1556
+ }
1557
+ }
1558
+ });
1559
+ }
1560
+ /**
1561
+ * Decorate raw image markdown when cursor is in range
1562
+ */
1563
+ decorateRawImage(node, decorations, view) {
1564
+ for (let child = node.firstChild; child; child = child.nextSibling) {
1565
+ if (child.name === "URL") {
1566
+ decorations.push(imageMarkDecorations["image-url"].range(child.from, child.to));
1567
+ }
1568
+ }
1569
+ const content = view.state.sliceDoc(node.from, node.to);
1570
+ const bangBracket = node.from;
1571
+ if (content.startsWith("![")) {
1572
+ decorations.push(imageMarkDecorations["image-marker"].range(bangBracket, bangBracket + 2));
1573
+ }
1574
+ const altEnd = content.indexOf("](");
1575
+ if (altEnd !== -1) {
1576
+ const altStart = 2;
1577
+ if (altEnd > altStart) {
1578
+ decorations.push(imageMarkDecorations["image-alt"].range(node.from + altStart, node.from + altEnd));
1579
+ }
1580
+ decorations.push(imageMarkDecorations["image-marker"].range(node.from + altEnd, node.from + altEnd + 2));
1581
+ decorations.push(imageMarkDecorations["image-marker"].range(node.to - 1, node.to));
1582
+ }
1583
+ }
1584
+ /**
1585
+ * Render image to HTML for preview mode using figure/figcaption
1586
+ */
1587
+ renderToHTML(node, _children, ctx) {
1588
+ if (node.name !== "Image") return null;
1589
+ const content = ctx.sliceDoc(node.from, node.to);
1590
+ const parsed = parseImageMarkdown(content);
1591
+ if (!parsed) return null;
1592
+ const altAttr = ctx.sanitize(parsed.alt);
1593
+ const titleAttr = parsed.title ? ` title="${ctx.sanitize(parsed.title)}"` : "";
1594
+ const ariaLabel = parsed.title ? ` aria-label="${ctx.sanitize(parsed.title)}"` : "";
1595
+ let html = `<figure class="cm-draftly-image-figure" role="figure"${ariaLabel}>`;
1596
+ html += `<img class="cm-draftly-image" src="${ctx.sanitize(parsed.url)}" alt="${altAttr}"${titleAttr} loading="lazy" decoding="async" />`;
1597
+ if (parsed.title) {
1598
+ html += `<figcaption class="cm-draftly-image-caption">${ctx.sanitize(parsed.title)}</figcaption>`;
1599
+ }
1600
+ html += `</figure>`;
1601
+ return html;
1602
+ }
1603
+ };
1604
+ var theme7 = createTheme({
1605
+ default: {
1606
+ ".cm-draftly-image-block br": {
1607
+ display: "none"
1608
+ },
1609
+ // Image markers (! [ ] ( ))
1610
+ ".cm-draftly-image-marker": {
1611
+ color: "#6a737d",
1612
+ fontFamily: "var(--font-jetbrains-mono, monospace)"
1613
+ },
1614
+ // Alt text
1615
+ ".cm-draftly-image-alt": {
1616
+ color: "#22863a",
1617
+ fontStyle: "italic"
1618
+ },
1619
+ // URL
1620
+ ".cm-draftly-image-url": {
1621
+ color: "#0366d6",
1622
+ textDecoration: "underline"
1623
+ },
1624
+ // Hidden markdown syntax (when cursor is not in range)
1625
+ ".cm-draftly-image-hidden": {
1626
+ display: "none"
1627
+ },
1628
+ // Figure container
1629
+ ".cm-draftly-image-figure": {
1630
+ display: "flex",
1631
+ flexDirection: "column",
1632
+ alignItems: "start",
1633
+ maxWidth: "100%",
1634
+ padding: "0"
1635
+ },
1636
+ // Image element
1637
+ ".cm-draftly-image": {
1638
+ maxWidth: "100%",
1639
+ maxHeight: "800px",
1640
+ height: "auto",
1641
+ borderRadius: "4px"
1642
+ },
1643
+ // Figcaption
1644
+ ".cm-draftly-image-caption": {
1645
+ display: "block",
1646
+ width: "100%",
1647
+ fontSize: "0.875em",
1648
+ color: "#6a737d",
1649
+ marginTop: "0.5em",
1650
+ textAlign: "center",
1651
+ fontStyle: "italic"
1652
+ },
1653
+ // Error state
1654
+ ".cm-draftly-image-error": {
1655
+ display: "inline-block",
1656
+ padding: "0.5em 1em",
1657
+ backgroundColor: "rgba(255, 0, 0, 0.1)",
1658
+ color: "#d73a49",
1659
+ borderRadius: "4px",
1660
+ fontSize: "0.875em",
1661
+ fontStyle: "italic"
1662
+ }
1663
+ },
1664
+ dark: {
1665
+ ".cm-draftly-image-marker": {
1666
+ color: "#8b949e"
1667
+ },
1668
+ ".cm-draftly-image-alt": {
1669
+ color: "#7ee787"
1670
+ },
1671
+ ".cm-draftly-image-url": {
1672
+ color: "#58a6ff"
1673
+ },
1674
+ ".cm-draftly-image-caption": {
1675
+ color: "#8b949e"
1676
+ },
1677
+ ".cm-draftly-image-error": {
1678
+ backgroundColor: "rgba(255, 0, 0, 0.15)",
1679
+ color: "#f85149"
1680
+ }
1681
+ }
1682
+ });
1683
+ function injectKatexStyles() {
1684
+ if (typeof document === "undefined") return;
1685
+ if (document.getElementById("draftly-katex-styles")) return;
1686
+ const style = document.createElement("style");
1687
+ style.id = "draftly-katex-styles";
1688
+ style.textContent = katexCss;
1689
+ document.head.appendChild(style);
1690
+ }
1691
+ injectKatexStyles();
1692
+ var DOLLAR = 36;
1693
+ var mathMarkDecorations = {
1694
+ "math-block": Decoration.line({ class: "cm-draftly-math-block" }),
1695
+ "math-inline": Decoration.mark({ class: "cm-draftly-math-inline" }),
1696
+ "math-marker": Decoration.mark({ class: "cm-draftly-math-marker" }),
1697
+ "math-hidden": Decoration.mark({ class: "cm-draftly-math-hidden" })
1698
+ };
1699
+ function renderMath(latex, displayMode) {
1700
+ try {
1701
+ const html = katex.renderToString(latex, {
1702
+ displayMode,
1703
+ throwOnError: false,
1704
+ errorColor: "#d73a49",
1705
+ trust: false,
1706
+ strict: false
1707
+ });
1708
+ return { html, error: null };
1709
+ } catch (e) {
1710
+ const errorMsg = e instanceof Error ? e.message : "Unknown error";
1711
+ return { html: "", error: errorMsg };
1712
+ }
1713
+ }
1714
+ var InlineMathWidget = class extends WidgetType {
1715
+ constructor(latex, from, to) {
1716
+ super();
1717
+ this.latex = latex;
1718
+ this.from = from;
1719
+ this.to = to;
1720
+ }
1721
+ eq(other) {
1722
+ return other.latex === this.latex && other.from === this.from && other.to === this.to;
1723
+ }
1724
+ toDOM(view) {
1725
+ const span = document.createElement("span");
1726
+ span.className = "cm-draftly-math-rendered cm-draftly-math-rendered-inline";
1727
+ span.style.cursor = "pointer";
1728
+ const { html, error } = renderMath(this.latex, false);
1729
+ if (error) {
1730
+ span.className += " cm-draftly-math-error";
1731
+ span.textContent = `[Math Error: ${error}]`;
1732
+ } else {
1733
+ span.innerHTML = html;
1734
+ }
1735
+ span.addEventListener("click", (e) => {
1736
+ e.preventDefault();
1737
+ e.stopPropagation();
1738
+ view.dispatch({
1739
+ selection: { anchor: this.from, head: this.to },
1740
+ scrollIntoView: true
1741
+ });
1742
+ view.focus();
1743
+ });
1744
+ return span;
1745
+ }
1746
+ ignoreEvent(event) {
1747
+ return event.type !== "click";
1748
+ }
1749
+ };
1750
+ var MathBlockWidget = class extends WidgetType {
1751
+ constructor(latex, from, to) {
1752
+ super();
1753
+ this.latex = latex;
1754
+ this.from = from;
1755
+ this.to = to;
1756
+ }
1757
+ eq(other) {
1758
+ return other.latex === this.latex && other.from === this.from && other.to === this.to;
1759
+ }
1760
+ toDOM(view) {
1761
+ const div = document.createElement("div");
1762
+ div.className = "cm-draftly-math-rendered cm-draftly-math-rendered-block";
1763
+ div.style.cursor = "pointer";
1764
+ const { html, error } = renderMath(this.latex, true);
1765
+ if (error) {
1766
+ div.className += " cm-draftly-math-error";
1767
+ div.textContent = `[Math Error: ${error}]`;
1768
+ } else {
1769
+ div.innerHTML = html;
1770
+ }
1771
+ div.addEventListener("click", (e) => {
1772
+ e.preventDefault();
1773
+ e.stopPropagation();
1774
+ view.dispatch({
1775
+ selection: { anchor: this.from, head: this.to },
1776
+ scrollIntoView: true
1777
+ });
1778
+ view.focus();
1779
+ });
1780
+ return div;
1781
+ }
1782
+ ignoreEvent(event) {
1783
+ return event.type !== "click";
1784
+ }
1785
+ };
1786
+ var inlineMathParser = {
1787
+ name: "InlineMath",
1788
+ parse(cx, next, pos) {
1789
+ if (next !== DOLLAR) return -1;
1790
+ if (cx.char(pos + 1) === DOLLAR) return -1;
1791
+ let end = pos + 1;
1792
+ while (end < cx.end) {
1793
+ const char = cx.char(end);
1794
+ if (char === DOLLAR) {
1795
+ if (cx.char(end + 1) !== DOLLAR) {
1796
+ const content = cx.slice(pos + 1, end);
1797
+ if (content.trim().length === 0) return -1;
1798
+ const openMark = cx.elt("InlineMathMark", pos, pos + 1);
1799
+ const closeMark = cx.elt("InlineMathMark", end, end + 1);
1800
+ const inlineMath = cx.elt("InlineMath", pos, end + 1, [openMark, closeMark]);
1801
+ return cx.addElement(inlineMath);
1802
+ }
1803
+ return -1;
1804
+ }
1805
+ if (char === 92) {
1806
+ end += 2;
1807
+ continue;
1808
+ }
1809
+ end++;
1810
+ }
1811
+ return -1;
1812
+ }
1813
+ };
1814
+ var mathBlockParser = {
1815
+ name: "MathBlock",
1816
+ parse(cx, line) {
1817
+ const text = line.text;
1818
+ const trimmed = text.slice(line.pos).trimStart();
1819
+ if (!trimmed.startsWith("$$")) return false;
1820
+ const startLine = cx.lineStart;
1821
+ let endPos = -1;
1822
+ let lastLineEnd = startLine + line.text.length;
1823
+ while (cx.nextLine()) {
1824
+ const currentText = line.text;
1825
+ lastLineEnd = cx.lineStart + currentText.length;
1826
+ if (currentText.trimEnd().endsWith("$$")) {
1827
+ endPos = lastLineEnd;
1828
+ cx.nextLine();
1829
+ break;
1830
+ }
1831
+ }
1832
+ if (endPos === -1) {
1833
+ return false;
1834
+ }
1835
+ const openMark = cx.elt("MathBlockMark", startLine, startLine + text.indexOf("$$") + 2);
1836
+ const closeMark = cx.elt("MathBlockMark", endPos - 2, endPos);
1837
+ cx.addElement(cx.elt("MathBlock", startLine, endPos, [openMark, closeMark]));
1838
+ return true;
1839
+ }
1840
+ };
1841
+ var MathPlugin = class extends DecorationPlugin {
1842
+ name = "math";
1843
+ version = "1.0.0";
1844
+ decorationPriority = 25;
1845
+ requiredNodes = ["InlineMath", "MathBlock", "InlineMathMark", "MathBlockMark"];
1846
+ constructor() {
1847
+ super();
1848
+ }
1849
+ /**
1850
+ * Plugin theme
1851
+ */
1852
+ get theme() {
1853
+ return theme8;
1854
+ }
1855
+ /**
1856
+ * Return markdown parser extensions for math syntax
1857
+ */
1858
+ getMarkdownConfig() {
1859
+ return {
1860
+ defineNodes: [
1861
+ { name: "InlineMath", style: tags.emphasis },
1862
+ { name: "InlineMathMark", style: tags.processingInstruction },
1863
+ { name: "MathBlock", block: true },
1864
+ { name: "MathBlockMark", style: tags.processingInstruction }
1865
+ ],
1866
+ parseInline: [inlineMathParser],
1867
+ parseBlock: [mathBlockParser]
1868
+ };
1869
+ }
1870
+ /**
1871
+ * Build decorations for math expressions
1872
+ */
1873
+ buildDecorations(ctx) {
1874
+ const { view, decorations } = ctx;
1875
+ const tree = syntaxTree(view.state);
1876
+ tree.iterate({
1877
+ enter: (node) => {
1878
+ const { from, to, name } = node;
1879
+ if (name === "InlineMath") {
1880
+ const content = view.state.sliceDoc(from, to);
1881
+ const latex = content.slice(1, -1);
1882
+ const cursorInRange = ctx.selectionOverlapsRange(from, to);
1883
+ if (cursorInRange) {
1884
+ decorations.push(mathMarkDecorations["math-inline"].range(from, to));
1885
+ for (let child = node.node.firstChild; child; child = child.nextSibling) {
1886
+ if (child.name === "InlineMathMark") {
1887
+ decorations.push(mathMarkDecorations["math-marker"].range(child.from, child.to));
1888
+ }
1889
+ }
1890
+ } else {
1891
+ decorations.push(
1892
+ Decoration.replace({
1893
+ widget: new InlineMathWidget(latex, from, to)
1894
+ }).range(from, to)
1895
+ );
1896
+ }
1897
+ }
1898
+ if (name === "MathBlock") {
1899
+ const content = view.state.sliceDoc(from, to);
1900
+ const lines = content.split("\n");
1901
+ const latex = lines.slice(1, -1).join("\n").trim();
1902
+ const singleLine = !content.includes("\n");
1903
+ const latexContent = singleLine ? content.slice(2, -2).trim() : latex;
1904
+ const nodeLineStart = view.state.doc.lineAt(from);
1905
+ const nodeLineEnd = view.state.doc.lineAt(to);
1906
+ const cursorInRange = ctx.selectionOverlapsRange(nodeLineStart.from, nodeLineEnd.to);
1907
+ decorations.push(mathMarkDecorations["math-block"].range(from));
1908
+ decorations.push(
1909
+ Decoration.widget({
1910
+ widget: new MathBlockWidget(latexContent, from, to),
1911
+ side: 1,
1912
+ block: false
1913
+ }).range(to)
1914
+ );
1915
+ for (let i = nodeLineStart.number; i <= nodeLineEnd.number; i++) {
1916
+ const line = view.state.doc.line(i);
1917
+ decorations.push(mathMarkDecorations["math-block"].range(line.from));
1918
+ }
1919
+ if (cursorInRange) {
1920
+ for (let child = node.node.firstChild; child; child = child.nextSibling) {
1921
+ if (child.name === "MathBlockMark") {
1922
+ decorations.push(mathMarkDecorations["math-marker"].range(child.from, child.to));
1923
+ }
1924
+ }
1925
+ } else {
1926
+ decorations.push(mathMarkDecorations["math-hidden"].range(from, to));
1927
+ }
1928
+ }
1929
+ }
1930
+ });
1931
+ }
1932
+ /**
1933
+ * Render math to HTML for preview mode
1934
+ */
1935
+ renderToHTML(node, _children, ctx) {
1936
+ if (node.name === "InlineMath") {
1937
+ const content = ctx.sliceDoc(node.from, node.to);
1938
+ const latex = content.slice(1, -1);
1939
+ const { html, error } = renderMath(latex, false);
1940
+ if (error) {
1941
+ return `<span class="cm-draftly-math-error">[Math Error: ${ctx.sanitize(error)}]</span>`;
1942
+ }
1943
+ return `<span class="cm-draftly-math-rendered cm-draftly-math-rendered-inline">${html}</span>`;
1944
+ }
1945
+ if (node.name === "MathBlock") {
1946
+ const content = ctx.sliceDoc(node.from, node.to);
1947
+ const lines = content.split("\n");
1948
+ const latex = lines.length > 1 ? lines.slice(1, -1).join("\n").trim() : content.slice(2, -2).trim();
1949
+ const { html, error } = renderMath(latex, true);
1950
+ if (error) {
1951
+ return `<div class="cm-draftly-math-error">[Math Error: ${ctx.sanitize(error)}]</div>`;
1952
+ }
1953
+ return `<div class="cm-draftly-math-rendered cm-draftly-math-rendered-block">${html}</div>`;
1954
+ }
1955
+ if (node.name === "InlineMathMark" || node.name === "MathBlockMark") {
1956
+ return "";
1957
+ }
1958
+ return null;
1959
+ }
1960
+ };
1961
+ var theme8 = createTheme({
1962
+ default: {
1963
+ ".cm-draftly-math-block": {
1964
+ fontFamily: "var(--font-jetbrains-mono, monospace)"
1965
+ },
1966
+ ".cm-draftly-math-block br": {
1967
+ display: "none"
1968
+ },
1969
+ // Math markers ($ $$)
1970
+ ".cm-draftly-math-marker": {
1971
+ color: "#6a737d",
1972
+ fontFamily: "var(--font-jetbrains-mono, monospace)"
1973
+ },
1974
+ // Inline math styling when editing
1975
+ ".cm-draftly-math-inline": {
1976
+ fontFamily: "var(--font-jetbrains-mono, monospace)",
1977
+ fontSize: "0.9em"
1978
+ },
1979
+ // Hidden math syntax (when cursor is not in range)
1980
+ ".cm-draftly-math-hidden": {
1981
+ display: "none"
1982
+ },
1983
+ // Hidden line (for multi-line blocks)
1984
+ ".cm-draftly-hidden-line": {
1985
+ display: "none"
1986
+ },
1987
+ // Rendered math container (both inline and block)
1988
+ ".cm-draftly-math-rendered": {
1989
+ fontFamily: "KaTeX_Main, 'Times New Roman', serif"
1990
+ },
1991
+ // Inline rendered math
1992
+ ".cm-draftly-math-rendered-inline": {
1993
+ display: "inline",
1994
+ verticalAlign: "baseline"
1995
+ },
1996
+ // Block rendered math (display mode)
1997
+ ".cm-draftly-math-rendered-block": {
1998
+ display: "flex",
1999
+ justifyContent: "center",
2000
+ alignItems: "center",
2001
+ padding: "1em 0",
2002
+ backgroundColor: "rgba(0, 0, 0, 0.02)",
2003
+ borderRadius: "4px",
2004
+ overflow: "auto"
2005
+ },
2006
+ // Math error styling
2007
+ ".cm-draftly-math-error": {
2008
+ display: "inline-block",
2009
+ padding: "0.25em 0.5em",
2010
+ backgroundColor: "rgba(255, 0, 0, 0.1)",
2011
+ color: "#d73a49",
2012
+ borderRadius: "4px",
2013
+ fontSize: "0.875em",
2014
+ fontStyle: "italic",
2015
+ fontFamily: "var(--font-jetbrains-mono, monospace)"
2016
+ }
2017
+ },
2018
+ dark: {
2019
+ ".cm-draftly-math-marker": {
2020
+ color: "#8b949e"
2021
+ },
2022
+ ".cm-draftly-math-rendered-block": {
2023
+ backgroundColor: "rgba(255, 255, 255, 0.02)"
2024
+ },
2025
+ ".cm-draftly-math-error": {
2026
+ backgroundColor: "rgba(255, 0, 0, 0.15)",
2027
+ color: "#f85149"
2028
+ }
2029
+ }
2030
+ });
2031
+ mermaid.initialize({
2032
+ startOnLoad: false,
2033
+ theme: "default",
2034
+ suppressErrorRendering: true
2035
+ });
2036
+ var mermaidCounter = 0;
2037
+ async function renderMermaid(definition, options = {}, defaultTheme = "default") {
2038
+ try {
2039
+ const id = `draftly-mermaid-${mermaidCounter++}`;
2040
+ let finalDefinition = definition;
2041
+ const mermaidConfig = {};
2042
+ if (options.theme) {
2043
+ mermaidConfig.theme = options.theme;
2044
+ } else {
2045
+ mermaidConfig.theme = defaultTheme;
2046
+ }
2047
+ if (Object.keys(mermaidConfig).length > 0) {
2048
+ const jsonConfig = JSON.stringify(mermaidConfig);
2049
+ finalDefinition = `%%{init: ${jsonConfig} }%%
2050
+ ${definition}`;
2051
+ }
2052
+ const { svg } = await mermaid.render(id, finalDefinition);
2053
+ return { svg, error: null };
2054
+ } catch (e) {
2055
+ const errorMsg = e instanceof Error ? e.message : "Unknown error";
2056
+ return { svg: "", error: errorMsg };
2057
+ }
2058
+ }
2059
+ function parseAttributes(fenceLine) {
2060
+ const attributes = {};
2061
+ const regex = /(\w+)=["']([^"']*)["']/g;
2062
+ let match;
2063
+ while ((match = regex.exec(fenceLine)) !== null && match[1] && match[2]) {
2064
+ attributes[match[1]] = match[2];
2065
+ }
2066
+ return attributes;
2067
+ }
2068
+ var mermaidMarkDecorations = {
2069
+ "mermaid-block-start": Decoration.line({ class: "cm-draftly-mermaid-block-start" }),
2070
+ "mermaid-block-end": Decoration.line({ class: "cm-draftly-mermaid-block-end" }),
2071
+ "mermaid-block": Decoration.line({ class: "cm-draftly-mermaid-block" }),
2072
+ "mermaid-block-rendered": Decoration.line({ class: "cm-draftly-mermaid-block-rendered" }),
2073
+ "mermaid-marker": Decoration.mark({ class: "cm-draftly-mermaid-marker" }),
2074
+ "mermaid-hidden": Decoration.mark({ class: "cm-draftly-mermaid-hidden" })
2075
+ };
2076
+ var MermaidBlockWidget = class extends WidgetType {
2077
+ constructor(definition, attributes, defaultTheme, from, to) {
2078
+ super();
2079
+ this.definition = definition;
2080
+ this.attributes = attributes;
2081
+ this.defaultTheme = defaultTheme;
2082
+ this.from = from;
2083
+ this.to = to;
2084
+ }
2085
+ eq(other) {
2086
+ return other.definition === this.definition && JSON.stringify(other.attributes) === JSON.stringify(this.attributes) && other.defaultTheme === this.defaultTheme && other.from === this.from && other.to === this.to;
2087
+ }
2088
+ toDOM(view) {
2089
+ const div = document.createElement("div");
2090
+ div.className = "cm-draftly-mermaid-rendered";
2091
+ div.style.cursor = "pointer";
2092
+ div.innerHTML = `<div class="cm-draftly-mermaid-loading">Rendering diagram\u2026</div>`;
2093
+ renderMermaid(this.definition, this.attributes, this.defaultTheme).then(({ svg, error }) => {
2094
+ if (error) {
2095
+ div.className += " cm-draftly-mermaid-error";
2096
+ div.innerHTML = `<span>[Mermaid Error: ${error}]</span>`;
2097
+ } else {
2098
+ div.innerHTML = svg;
2099
+ }
2100
+ });
2101
+ div.addEventListener("click", (e) => {
2102
+ e.preventDefault();
2103
+ e.stopPropagation();
2104
+ view.dispatch({
2105
+ selection: { anchor: this.from, head: this.to },
2106
+ scrollIntoView: true
2107
+ });
2108
+ view.focus();
2109
+ });
2110
+ return div;
2111
+ }
2112
+ ignoreEvent(event) {
2113
+ return event.type !== "click";
2114
+ }
2115
+ };
2116
+ var mermaidBlockParser = {
2117
+ name: "MermaidBlock",
2118
+ before: "FencedCode",
2119
+ parse(cx, line) {
2120
+ const text = line.text;
2121
+ const trimmed = text.slice(line.pos).trimStart();
2122
+ if (!trimmed.startsWith("```mermaid")) return false;
2123
+ const startLine = cx.lineStart;
2124
+ let endPos = -1;
2125
+ let closeBacktickStart = -1;
2126
+ while (cx.nextLine()) {
2127
+ const currentText = line.text;
2128
+ const currentLineStart = cx.lineStart;
2129
+ const lastLineEnd = currentLineStart + currentText.length;
2130
+ const trimmedLine = currentText.trim();
2131
+ if (trimmedLine === "```") {
2132
+ endPos = lastLineEnd;
2133
+ closeBacktickStart = currentLineStart + currentText.indexOf("```");
2134
+ cx.nextLine();
2135
+ break;
2136
+ }
2137
+ }
2138
+ if (endPos === -1) {
2139
+ return false;
2140
+ }
2141
+ const openMarkEnd = startLine + text.indexOf("```mermaid") + 10;
2142
+ const openMark = cx.elt("MermaidBlockMark", startLine, openMarkEnd);
2143
+ const closeMark = cx.elt("MermaidBlockMark", closeBacktickStart, closeBacktickStart + 3);
2144
+ cx.addElement(cx.elt("MermaidBlock", startLine, endPos, [openMark, closeMark]));
2145
+ return true;
2146
+ }
2147
+ };
2148
+ var MermaidPlugin = class extends DecorationPlugin {
2149
+ name = "mermaid";
2150
+ version = "1.0.0";
2151
+ decorationPriority = 25;
2152
+ requiredNodes = ["MermaidBlock", "MermaidBlockMark"];
2153
+ constructor() {
2154
+ super();
2155
+ }
2156
+ /**
2157
+ * Plugin theme
2158
+ */
2159
+ get theme() {
2160
+ return theme9;
2161
+ }
2162
+ /**
2163
+ * Return markdown parser extensions for mermaid syntax
2164
+ */
2165
+ getMarkdownConfig() {
2166
+ return {
2167
+ defineNodes: [
2168
+ { name: "MermaidBlock", block: true },
2169
+ { name: "MermaidBlockMark", style: tags.processingInstruction }
2170
+ ],
2171
+ parseBlock: [mermaidBlockParser]
2172
+ };
2173
+ }
2174
+ /**
2175
+ * Build decorations for mermaid blocks
2176
+ */
2177
+ buildDecorations(ctx) {
2178
+ const { view, decorations } = ctx;
2179
+ const tree = syntaxTree(view.state);
2180
+ const config = this.context?.config;
2181
+ const currentTheme = config?.theme === "dark" /* DARK */ ? "dark" : "default";
2182
+ tree.iterate({
2183
+ enter: (node) => {
2184
+ const { from, to, name } = node;
2185
+ if (name === "MermaidBlock") {
2186
+ const content = view.state.sliceDoc(from, to);
2187
+ const lines = content.split("\n");
2188
+ const definition = lines.slice(1, -1).join("\n").trim();
2189
+ const docLines = content.split("\n");
2190
+ const fenceLine = docLines[0] || "";
2191
+ const attributes = parseAttributes(fenceLine);
2192
+ const nodeLineStart = view.state.doc.lineAt(from);
2193
+ const nodeLineEnd = view.state.doc.lineAt(to);
2194
+ const cursorInRange = ctx.selectionOverlapsRange(nodeLineStart.from, nodeLineEnd.to);
2195
+ const totalCodeLines = nodeLineEnd.number - nodeLineStart.number - 1;
2196
+ const lineNumWidth = String(totalCodeLines).length;
2197
+ let codeLineIndex = 1;
2198
+ for (let i = nodeLineStart.number; i <= nodeLineEnd.number; i++) {
2199
+ const line = view.state.doc.line(i);
2200
+ const isFenceLine = i === nodeLineStart.number || i === nodeLineEnd.number;
2201
+ const relativeLineNum = codeLineIndex;
2202
+ decorations.push(mermaidMarkDecorations["mermaid-block"].range(line.from));
2203
+ if (!cursorInRange) decorations.push(mermaidMarkDecorations["mermaid-block-rendered"].range(line.from));
2204
+ if (i === nodeLineStart.number)
2205
+ decorations.push(mermaidMarkDecorations["mermaid-block-start"].range(line.from));
2206
+ if (i === nodeLineEnd.number)
2207
+ decorations.push(mermaidMarkDecorations["mermaid-block-end"].range(line.from));
2208
+ if (!isFenceLine) {
2209
+ decorations.push(
2210
+ Decoration.line({
2211
+ attributes: {
2212
+ "data-line-num": String(relativeLineNum),
2213
+ style: `--line-num-width: ${lineNumWidth}ch`
2214
+ }
2215
+ }).range(line.from)
2216
+ );
2217
+ }
2218
+ if (!isFenceLine) {
2219
+ codeLineIndex++;
2220
+ }
2221
+ }
2222
+ decorations.push(
2223
+ Decoration.widget({
2224
+ widget: new MermaidBlockWidget(definition, attributes, currentTheme, from, to),
2225
+ side: 1,
2226
+ block: false
2227
+ }).range(to)
2228
+ );
2229
+ if (cursorInRange) {
2230
+ for (let child = node.node.firstChild; child; child = child.nextSibling) {
2231
+ if (child.name === "MermaidBlockMark") {
2232
+ decorations.push(mermaidMarkDecorations["mermaid-marker"].range(child.from, child.to));
2233
+ }
2234
+ }
2235
+ } else {
2236
+ decorations.push(mermaidMarkDecorations["mermaid-hidden"].range(from, to));
2237
+ }
2238
+ }
2239
+ }
2240
+ });
2241
+ }
2242
+ /**
2243
+ * Render mermaid to HTML for preview mode
2244
+ *
2245
+ * Renders the actual mermaid diagram to SVG HTML
2246
+ */
2247
+ async renderToHTML(node, _children, ctx) {
2248
+ if (node.name === "MermaidBlock") {
2249
+ const content = ctx.sliceDoc(node.from, node.to);
2250
+ const lines = content.split("\n");
2251
+ const definition = lines.length > 1 ? lines.slice(1, -1).join("\n").trim() : "";
2252
+ const fenceLine = lines[0] || "";
2253
+ const attributes = parseAttributes(fenceLine);
2254
+ const config = this.context?.config;
2255
+ const currentTheme = config?.theme === "dark" /* DARK */ ? "dark" : "default";
2256
+ const { svg, error } = await renderMermaid(definition, attributes, currentTheme);
2257
+ if (error) {
2258
+ return `<div class="cm-draftly-mermaid-error">${ctx.sanitize(`[Mermaid Error: ${error}]`)}</div>`;
2259
+ }
2260
+ return `<div class="cm-draftly-mermaid-rendered">${svg}</div>`;
2261
+ }
2262
+ if (node.name === "MermaidBlockMark") {
2263
+ return "";
2264
+ }
2265
+ return null;
2266
+ }
2267
+ };
2268
+ var theme9 = createTheme({
2269
+ default: {
2270
+ // Raw mermaid block lines (monospace)
2271
+ ".cm-draftly-mermaid-block:not(.cm-draftly-mermaid-block-rendered)": {
2272
+ "--radius": "0.375rem",
2273
+ position: "relative",
2274
+ fontFamily: "var(--font-jetbrains-mono, monospace)",
2275
+ fontSize: "0.9rem",
2276
+ backgroundColor: "rgba(0, 0, 0, 0.03)",
2277
+ padding: "0 1rem !important",
2278
+ paddingLeft: "calc(var(--line-num-width, 2ch) + 1rem) !important",
2279
+ lineHeight: "1.5",
2280
+ borderLeft: "1px solid var(--color-border)",
2281
+ borderRight: "1px solid var(--color-border)"
2282
+ },
2283
+ ".cm-draftly-mermaid-block-start:not(.cm-draftly-mermaid-block-rendered)": {
2284
+ overflow: "hidden",
2285
+ borderTopLeftRadius: "var(--radius)",
2286
+ borderTopRightRadius: "var(--radius)",
2287
+ borderTop: "1px solid var(--color-border)"
2288
+ },
2289
+ ".cm-draftly-mermaid-block-end:not(.cm-draftly-mermaid-block-rendered)": {
2290
+ overflow: "hidden",
2291
+ borderBottomLeftRadius: "var(--radius)",
2292
+ borderBottomRightRadius: "var(--radius)",
2293
+ borderBottom: "1px solid var(--color-border)"
2294
+ },
2295
+ ".cm-draftly-mermaid-block:not(.cm-draftly-mermaid-block-rendered)::before": {
2296
+ content: "attr(data-line-num)",
2297
+ position: "absolute",
2298
+ left: "0.5rem",
2299
+ top: "0.2rem",
2300
+ width: "var(--line-num-width, 2ch)",
2301
+ textAlign: "right",
2302
+ color: "#6a737d",
2303
+ opacity: "0.6",
2304
+ fontFamily: "var(--font-jetbrains-mono, monospace)",
2305
+ fontSize: "0.85rem",
2306
+ userSelect: "none"
2307
+ },
2308
+ ".cm-draftly-mermaid-block.cm-draftly-mermaid-block-rendered br": {
2309
+ display: "none"
2310
+ },
2311
+ // Mermaid markers (```mermaid / ```)
2312
+ ".cm-draftly-mermaid-marker": {
2313
+ color: "#6a737d",
2314
+ fontFamily: "var(--font-jetbrains-mono, monospace)"
2315
+ },
2316
+ // Hidden mermaid syntax (when cursor is not in range)
2317
+ ".cm-draftly-mermaid-hidden": {
2318
+ display: "none"
2319
+ },
2320
+ // Rendered mermaid container
2321
+ ".cm-draftly-mermaid-rendered": {
2322
+ display: "flex",
2323
+ justifyContent: "center",
2324
+ alignItems: "center",
2325
+ padding: "1em 0",
2326
+ borderRadius: "4px",
2327
+ overflow: "auto"
2328
+ },
2329
+ // SVG inside rendered container
2330
+ ".cm-draftly-mermaid-rendered svg": {
2331
+ maxWidth: "100%",
2332
+ height: "auto",
2333
+ aspectRatio: "auto"
2334
+ },
2335
+ // Loading state
2336
+ ".cm-draftly-mermaid-loading": {
2337
+ display: "inline-block",
2338
+ padding: "0.5em 1em",
2339
+ color: "#6a737d",
2340
+ fontSize: "0.875em",
2341
+ fontStyle: "italic",
2342
+ fontFamily: "var(--font-jetbrains-mono, monospace)"
2343
+ },
2344
+ // Error styling
2345
+ ".cm-draftly-mermaid-error": {
2346
+ display: "inline-block",
2347
+ padding: "0.25em 0.5em",
2348
+ backgroundColor: "rgba(255, 0, 0, 0.1)",
2349
+ color: "#d73a49",
2350
+ borderRadius: "4px",
2351
+ fontSize: "0.875em",
2352
+ fontStyle: "italic",
2353
+ fontFamily: "var(--font-jetbrains-mono, monospace)"
2354
+ }
2355
+ },
2356
+ dark: {
2357
+ ".cm-draftly-mermaid-block:not(.cm-draftly-mermaid-block-rendered)": {
2358
+ backgroundColor: "rgba(255, 255, 255, 0.03)"
2359
+ },
2360
+ ".cm-draftly-mermaid-marker": {
2361
+ color: "#8b949e"
2362
+ },
2363
+ ".cm-draftly-mermaid-loading": {
2364
+ color: "#8b949e"
2365
+ },
2366
+ ".cm-draftly-mermaid-error": {
2367
+ backgroundColor: "rgba(255, 0, 0, 0.15)",
2368
+ color: "#f85149"
2369
+ }
2370
+ }
2371
+ });
2372
+ var COPY_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>`;
2373
+ var CHECK_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>`;
2374
+ var COPY_RESET_DELAY = 2e3;
2375
+ var codeMarkDecorations = {
2376
+ // Inline code
2377
+ "inline-code": Decoration.mark({ class: "cm-draftly-code-inline" }),
2378
+ "inline-mark": Decoration.replace({}),
2379
+ // Fenced code block
2380
+ "code-block-line": Decoration.line({ class: "cm-draftly-code-block-line" }),
2381
+ "code-block-line-start": Decoration.line({ class: "cm-draftly-code-block-line-start" }),
2382
+ "code-block-line-end": Decoration.line({ class: "cm-draftly-code-block-line-end" }),
2383
+ "code-fence": Decoration.mark({ class: "cm-draftly-code-fence" }),
2384
+ "code-hidden": Decoration.replace({}),
2385
+ // Highlights
2386
+ "code-line-highlight": Decoration.line({ class: "cm-draftly-code-line-highlight" }),
2387
+ "code-text-highlight": Decoration.mark({ class: "cm-draftly-code-text-highlight" })
2388
+ };
2389
+ var CodeBlockHeaderWidget = class extends WidgetType {
2390
+ constructor(props, codeContent) {
2391
+ super();
2392
+ this.props = props;
2393
+ this.codeContent = codeContent;
2394
+ }
2395
+ /** Creates the header DOM element with title/language and optional copy button. */
2396
+ toDOM() {
2397
+ const header = document.createElement("div");
2398
+ header.className = "cm-draftly-code-header";
2399
+ const leftSide = document.createElement("div");
2400
+ leftSide.className = "cm-draftly-code-header-left";
2401
+ if (this.props.title) {
2402
+ const title = document.createElement("span");
2403
+ title.className = "cm-draftly-code-header-title";
2404
+ title.textContent = this.props.title;
2405
+ leftSide.appendChild(title);
2406
+ } else if (this.props.language) {
2407
+ const lang = document.createElement("span");
2408
+ lang.className = "cm-draftly-code-header-lang";
2409
+ lang.textContent = this.props.language;
2410
+ leftSide.appendChild(lang);
2411
+ }
2412
+ header.appendChild(leftSide);
2413
+ if (this.props.copy !== false) {
2414
+ const rightSide = document.createElement("div");
2415
+ rightSide.className = "cm-draftly-code-header-right";
2416
+ const copyBtn = document.createElement("button");
2417
+ copyBtn.className = "cm-draftly-code-copy-btn";
2418
+ copyBtn.type = "button";
2419
+ copyBtn.title = "Copy code";
2420
+ copyBtn.innerHTML = COPY_ICON;
2421
+ copyBtn.addEventListener("click", (e) => {
2422
+ e.preventDefault();
2423
+ e.stopPropagation();
2424
+ navigator.clipboard.writeText(this.codeContent).then(() => {
2425
+ copyBtn.classList.add("copied");
2426
+ copyBtn.innerHTML = CHECK_ICON;
2427
+ setTimeout(() => {
2428
+ copyBtn.classList.remove("copied");
2429
+ copyBtn.innerHTML = COPY_ICON;
2430
+ }, COPY_RESET_DELAY);
2431
+ });
2432
+ });
2433
+ rightSide.appendChild(copyBtn);
2434
+ header.appendChild(rightSide);
2435
+ }
2436
+ return header;
2437
+ }
2438
+ /** Checks equality for widget reuse optimization. */
2439
+ eq(other) {
2440
+ return this.props.title === other.props.title && this.props.language === other.props.language && this.props.copy === other.props.copy && this.codeContent === other.codeContent;
2441
+ }
2442
+ /** Allow click events to propagate for copy button interaction. */
2443
+ ignoreEvent() {
2444
+ return false;
2445
+ }
2446
+ };
2447
+ var CodeBlockCaptionWidget = class extends WidgetType {
2448
+ constructor(caption) {
2449
+ super();
2450
+ this.caption = caption;
2451
+ }
2452
+ /** Creates the caption DOM element. */
2453
+ toDOM() {
2454
+ const captionEl = document.createElement("div");
2455
+ captionEl.className = "cm-draftly-code-caption";
2456
+ captionEl.textContent = this.caption;
2457
+ return captionEl;
2458
+ }
2459
+ /** Checks equality for widget reuse optimization. */
2460
+ eq(other) {
2461
+ return this.caption === other.caption;
2462
+ }
2463
+ /** Allow click events to propagate for caption interaction. */
2464
+ ignoreEvent() {
2465
+ return false;
2466
+ }
2467
+ };
2468
+ var CodePlugin = class extends DecorationPlugin {
2469
+ name = "code";
2470
+ version = "1.0.0";
2471
+ decorationPriority = 25;
2472
+ requiredNodes = ["InlineCode", "FencedCode", "CodeMark", "CodeInfo", "CodeText"];
2473
+ /**
2474
+ * Plugin theme
2475
+ */
2476
+ get theme() {
2477
+ return theme10;
2478
+ }
2479
+ /**
2480
+ * Keyboard shortcuts for code formatting
2481
+ */
2482
+ getKeymap() {
2483
+ return [
2484
+ {
2485
+ key: "Mod-e",
2486
+ run: toggleMarkdownStyle("`"),
2487
+ preventDefault: true
2488
+ },
2489
+ {
2490
+ key: "Mod-Shift-e",
2491
+ run: (view) => this.toggleCodeBlock(view),
2492
+ preventDefault: true
2493
+ }
2494
+ ];
2495
+ }
2496
+ /**
2497
+ * Toggle code block on current line or selected lines
2498
+ */
2499
+ toggleCodeBlock(view) {
2500
+ const { state } = view;
2501
+ const { from, to } = state.selection.main;
2502
+ const startLine = state.doc.lineAt(from);
2503
+ const endLine = state.doc.lineAt(to);
2504
+ const prevLineNum = startLine.number > 1 ? startLine.number - 1 : startLine.number;
2505
+ const nextLineNum = endLine.number < state.doc.lines ? endLine.number + 1 : endLine.number;
2506
+ const prevLine = state.doc.line(prevLineNum);
2507
+ const nextLine = state.doc.line(nextLineNum);
2508
+ const isWrapped = prevLine.text.trim().startsWith("```") && nextLine.text.trim() === "```" && prevLineNum !== startLine.number && nextLineNum !== endLine.number;
2509
+ if (isWrapped) {
2510
+ view.dispatch({
2511
+ changes: [
2512
+ { from: prevLine.from, to: prevLine.to + 1, insert: "" },
2513
+ // Remove opening fence + newline
2514
+ { from: nextLine.from - 1, to: nextLine.to, insert: "" }
2515
+ // Remove newline + closing fence
2516
+ ]
2517
+ });
2518
+ } else {
2519
+ const openFence = "```\n";
2520
+ const closeFence = "\n```";
2521
+ view.dispatch({
2522
+ changes: [
2523
+ { from: startLine.from, insert: openFence },
2524
+ { from: endLine.to, insert: closeFence }
2525
+ ],
2526
+ selection: { anchor: startLine.from + openFence.length, head: endLine.to + openFence.length }
2527
+ });
2528
+ }
2529
+ return true;
2530
+ }
2531
+ /**
2532
+ * Parse CodeInfo string into structured properties
2533
+ *
2534
+ * @param codeInfo - The raw CodeInfo string (e.g., "tsx line-numbers{5} title=\"hello.tsx\" copy {2-4,5} /Hello/3-5")
2535
+ * @returns Parsed CodeBlockProperties object
2536
+ *
2537
+ * @example
2538
+ * ```typescript
2539
+ * parseCodeInfo("tsx line-numbers{5} title=\"hello.tsx\" copy {2-4,5} /Hello/3-5")
2540
+ * ```
2541
+ *
2542
+ * Returns:
2543
+ * ```json
2544
+ * {
2545
+ * language: "tsx",
2546
+ * lineNumbers: 5,
2547
+ * title: "hello.tsx",
2548
+ * copy: true,
2549
+ * highlightLines: [2,3,4,5],
2550
+ * highlightText: [{ pattern: "Hello", instances: [3,4,5] }]
2551
+ * }
2552
+ * ```
2553
+ */
2554
+ parseCodeInfo(codeInfo) {
2555
+ const props = { language: "" };
2556
+ if (!codeInfo || !codeInfo.trim()) {
2557
+ return props;
2558
+ }
2559
+ let remaining = codeInfo.trim();
2560
+ const langMatch = remaining.match(/^(\w+)/);
2561
+ if (langMatch && langMatch[1]) {
2562
+ props.language = langMatch[1];
2563
+ remaining = remaining.slice(langMatch[0].length).trim();
2564
+ }
2565
+ const quotedPattern = /(\w+)="([^"]*)"/g;
2566
+ let quotedMatch;
2567
+ while ((quotedMatch = quotedPattern.exec(remaining)) !== null) {
2568
+ const key = quotedMatch[1]?.toLowerCase();
2569
+ const value = quotedMatch[2];
2570
+ if (key === "title" && value !== void 0) {
2571
+ props.title = value;
2572
+ } else if (key === "caption" && value !== void 0) {
2573
+ props.caption = value;
2574
+ }
2575
+ }
2576
+ remaining = remaining.replace(quotedPattern, "").trim();
2577
+ const lineNumbersMatch = remaining.match(/line-numbers(?:\{(\d+)\})?/);
2578
+ if (lineNumbersMatch) {
2579
+ if (lineNumbersMatch[1]) {
2580
+ props.lineNumbers = parseInt(lineNumbersMatch[1], 10);
2581
+ } else {
2582
+ props.lineNumbers = true;
2583
+ }
2584
+ remaining = remaining.replace(lineNumbersMatch[0], "").trim();
2585
+ }
2586
+ if (/\bcopy\b/.test(remaining)) {
2587
+ props.copy = true;
2588
+ remaining = remaining.replace(/\bcopy\b/, "").trim();
2589
+ }
2590
+ const lineHighlightMatch = remaining.match(/\{([^}]+)\}/);
2591
+ if (lineHighlightMatch && lineHighlightMatch[1]) {
2592
+ const highlightLines = [];
2593
+ const parts = lineHighlightMatch[1].split(",");
2594
+ for (const part of parts) {
2595
+ const trimmed = part.trim();
2596
+ const rangeMatch = trimmed.match(/^(\d+)-(\d+)$/);
2597
+ if (rangeMatch && rangeMatch[1] && rangeMatch[2]) {
2598
+ const start = parseInt(rangeMatch[1], 10);
2599
+ const end = parseInt(rangeMatch[2], 10);
2600
+ for (let i = start; i <= end; i++) {
2601
+ highlightLines.push(i);
2602
+ }
2603
+ } else if (/^\d+$/.test(trimmed)) {
2604
+ highlightLines.push(parseInt(trimmed, 10));
2605
+ }
2606
+ }
2607
+ if (highlightLines.length > 0) {
2608
+ props.highlightLines = highlightLines;
2609
+ }
2610
+ remaining = remaining.replace(lineHighlightMatch[0], "").trim();
2611
+ }
2612
+ const textHighlightPattern = /\/([^/]+)\/(?:(\d+(?:-\d+)?(?:,\d+(?:-\d+)?)*))?/g;
2613
+ let textMatch;
2614
+ const highlightText = [];
2615
+ while ((textMatch = textHighlightPattern.exec(remaining)) !== null) {
2616
+ if (!textMatch[1]) continue;
2617
+ const highlight = {
2618
+ pattern: textMatch[1]
2619
+ };
2620
+ if (textMatch[2]) {
2621
+ const instanceStr = textMatch[2];
2622
+ const instances = [];
2623
+ const instanceParts = instanceStr.split(",");
2624
+ for (const part of instanceParts) {
2625
+ const rangeMatch = part.match(/^(\d+)-(\d+)$/);
2626
+ if (rangeMatch && rangeMatch[1] && rangeMatch[2]) {
2627
+ const start = parseInt(rangeMatch[1], 10);
2628
+ const end = parseInt(rangeMatch[2], 10);
2629
+ for (let i = start; i <= end; i++) {
2630
+ instances.push(i);
2631
+ }
2632
+ } else if (/^\d+$/.test(part)) {
2633
+ instances.push(parseInt(part, 10));
2634
+ }
2635
+ }
2636
+ if (instances.length > 0) {
2637
+ highlight.instances = instances;
2638
+ }
2639
+ }
2640
+ highlightText.push(highlight);
2641
+ }
2642
+ if (highlightText.length > 0) {
2643
+ props.highlightText = highlightText;
2644
+ }
2645
+ return props;
2646
+ }
2647
+ /**
2648
+ * Build decorations for inline code and fenced code blocks.
2649
+ * Handles line numbers, highlights, header/caption widgets, and fence visibility.
2650
+ */
2651
+ buildDecorations(ctx) {
2652
+ const { view, decorations } = ctx;
2653
+ const tree = syntaxTree(view.state);
2654
+ tree.iterate({
2655
+ enter: (node) => {
2656
+ const { from, to, name } = node;
2657
+ if (name === "InlineCode") {
2658
+ decorations.push(codeMarkDecorations["inline-code"].range(from, to));
2659
+ const cursorInRange = ctx.selectionOverlapsRange(from, to);
2660
+ if (!cursorInRange) {
2661
+ for (let child = node.node.firstChild; child; child = child.nextSibling) {
2662
+ if (child.name === "CodeMark") {
2663
+ decorations.push(codeMarkDecorations["inline-mark"].range(child.from, child.to));
2664
+ }
2665
+ }
2666
+ }
2667
+ }
2668
+ if (name === "FencedCode") {
2669
+ const nodeLineStart = view.state.doc.lineAt(from);
2670
+ const nodeLineEnd = view.state.doc.lineAt(to);
2671
+ const cursorInRange = ctx.selectionOverlapsRange(nodeLineStart.from, nodeLineEnd.to);
2672
+ let infoProps = { language: "" };
2673
+ for (let child = node.node.firstChild; child; child = child.nextSibling) {
2674
+ if (child.name === "CodeInfo") {
2675
+ infoProps = this.parseCodeInfo(view.state.sliceDoc(child.from, child.to).trim());
2676
+ break;
2677
+ }
2678
+ }
2679
+ const totalCodeLines = nodeLineEnd.number - nodeLineStart.number - 1;
2680
+ const startLineNum = typeof infoProps.lineNumbers === "number" ? infoProps.lineNumbers : 1;
2681
+ const maxLineNum = startLineNum + totalCodeLines - 1;
2682
+ const lineNumWidth = Math.max(String(maxLineNum).length, String(startLineNum).length);
2683
+ let codeLineIndex = 0;
2684
+ let codeContent = "";
2685
+ for (let child = node.node.firstChild; child; child = child.nextSibling) {
2686
+ if (child.name === "CodeText") {
2687
+ codeContent = view.state.sliceDoc(child.from, child.to);
2688
+ break;
2689
+ }
2690
+ }
2691
+ const shouldShowHeader = !cursorInRange && (infoProps.title || infoProps.copy || infoProps.language);
2692
+ const shouldShowCaption = !cursorInRange && infoProps.caption;
2693
+ if (shouldShowHeader) {
2694
+ decorations.push(
2695
+ Decoration.widget({
2696
+ widget: new CodeBlockHeaderWidget(infoProps, codeContent),
2697
+ block: false
2698
+ }).range(nodeLineStart.from)
2699
+ );
2700
+ }
2701
+ for (let i = nodeLineStart.number; i <= nodeLineEnd.number; i++) {
2702
+ const line = view.state.doc.line(i);
2703
+ const isFenceLine = i === nodeLineStart.number || i === nodeLineEnd.number;
2704
+ const relativeLineNum = startLineNum + codeLineIndex;
2705
+ decorations.push(codeMarkDecorations["code-block-line"].range(line.from));
2706
+ if (i === nodeLineStart.number) {
2707
+ decorations.push(codeMarkDecorations["code-block-line-start"].range(line.from));
2708
+ if (shouldShowHeader) {
2709
+ decorations.push(
2710
+ Decoration.line({
2711
+ class: "cm-draftly-code-block-has-header"
2712
+ }).range(line.from)
2713
+ );
2714
+ }
2715
+ }
2716
+ if (i === nodeLineEnd.number) {
2717
+ decorations.push(codeMarkDecorations["code-block-line-end"].range(line.from));
2718
+ if (shouldShowCaption) {
2719
+ decorations.push(
2720
+ Decoration.line({
2721
+ class: "cm-draftly-code-block-has-caption"
2722
+ }).range(line.from)
2723
+ );
2724
+ }
2725
+ }
2726
+ if (!isFenceLine && infoProps.lineNumbers) {
2727
+ decorations.push(
2728
+ Decoration.line({
2729
+ class: "cm-draftly-code-line-numbered",
2730
+ attributes: {
2731
+ "data-line-num": String(relativeLineNum),
2732
+ style: `--line-num-width: ${lineNumWidth}ch`
2733
+ }
2734
+ }).range(line.from)
2735
+ );
2736
+ }
2737
+ if (!isFenceLine && infoProps.highlightLines) {
2738
+ if (infoProps.highlightLines.includes(codeLineIndex + 1)) {
2739
+ decorations.push(codeMarkDecorations["code-line-highlight"].range(line.from));
2740
+ }
2741
+ }
2742
+ if (!isFenceLine && infoProps.highlightText && infoProps.highlightText.length > 0) {
2743
+ const lineText = view.state.sliceDoc(line.from, line.to);
2744
+ for (const textHighlight of infoProps.highlightText) {
2745
+ try {
2746
+ const regex = new RegExp(textHighlight.pattern, "g");
2747
+ let match;
2748
+ let matchIndex = 0;
2749
+ while ((match = regex.exec(lineText)) !== null) {
2750
+ matchIndex++;
2751
+ const shouldHighlight = !textHighlight.instances || textHighlight.instances.includes(matchIndex);
2752
+ if (shouldHighlight) {
2753
+ const matchFrom = line.from + match.index;
2754
+ const matchTo = matchFrom + match[0].length;
2755
+ decorations.push(codeMarkDecorations["code-text-highlight"].range(matchFrom, matchTo));
2756
+ }
2757
+ }
2758
+ } catch {
2759
+ }
2760
+ }
2761
+ }
2762
+ if (!isFenceLine) {
2763
+ codeLineIndex++;
2764
+ }
2765
+ }
2766
+ for (let child = node.node.firstChild; child; child = child.nextSibling) {
2767
+ if (child.name === "CodeMark" || child.name === "CodeInfo") {
2768
+ if (cursorInRange) {
2769
+ decorations.push(codeMarkDecorations["code-fence"].range(child.from, child.to));
2770
+ } else {
2771
+ decorations.push(codeMarkDecorations["code-hidden"].range(child.from, child.to));
2772
+ }
2773
+ }
2774
+ }
2775
+ if (!cursorInRange && infoProps.caption) {
2776
+ decorations.push(
2777
+ Decoration.widget({
2778
+ widget: new CodeBlockCaptionWidget(infoProps.caption),
2779
+ block: false,
2780
+ side: 1
2781
+ // After the content
2782
+ }).range(nodeLineEnd.to)
2783
+ );
2784
+ }
2785
+ }
2786
+ }
2787
+ });
2788
+ }
2789
+ /**
2790
+ * Render code elements to HTML for static preview.
2791
+ * Applies syntax highlighting using @lezer/highlight.
2792
+ */
2793
+ renderToHTML(node, children, ctx) {
2794
+ if (node.name === "CodeMark") {
2795
+ return "";
2796
+ }
2797
+ if (node.name === "InlineCode") {
2798
+ let content = ctx.sliceDoc(node.from, node.to);
2799
+ const match = content.match(/^`+(.+?)`+$/s);
2800
+ if (match && match[1]) {
2801
+ content = match[1];
2802
+ }
2803
+ return `<code class="cm-draftly-code-inline" style="padding: 0.1rem 0.25rem">${ctx.sanitize(content)}</code>`;
2804
+ }
2805
+ if (node.name === "FencedCode") {
2806
+ const content = ctx.sliceDoc(node.from, node.to);
2807
+ const lines = content.split("\n");
2808
+ const firstLine = lines[0] || "";
2809
+ const infoMatch = firstLine.match(/^```(.*)$/);
2810
+ const infoString = infoMatch?.[1]?.trim() || "";
2811
+ const props = this.parseCodeInfo(infoString);
2812
+ const codeLines = lines.slice(1, -1);
2813
+ const code = codeLines.join("\n");
2814
+ let html = "";
2815
+ html += `<div class="cm-draftly-code-container">`;
2816
+ const showHeader = props.title || props.copy || props.language;
2817
+ if (showHeader) {
2818
+ html += `<div class="cm-draftly-code-header">`;
2819
+ html += `<div class="cm-draftly-code-header-left">`;
2820
+ if (props.title) {
2821
+ html += `<span class="cm-draftly-code-header-title">${ctx.sanitize(props.title)}</span>`;
2822
+ } else if (props.language) {
2823
+ html += `<span class="cm-draftly-code-header-lang">${ctx.sanitize(props.language)}</span>`;
2824
+ }
2825
+ html += `</div>`;
2826
+ if (props.copy !== false) {
2827
+ html += `<div class="cm-draftly-code-header-right">`;
2828
+ const encodedCode = typeof btoa !== "undefined" ? btoa(encodeURIComponent(code)) : Buffer.from(code).toString("base64");
2829
+ html += `<button class="cm-draftly-code-copy-btn" type="button" title="Copy code" data-code="${encodedCode}" data-encoded="true">`;
2830
+ html += `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>`;
2831
+ html += `</button>`;
2832
+ html += `</div>`;
2833
+ }
2834
+ html += `</div>`;
2835
+ }
2836
+ const startLineNum = typeof props.lineNumbers === "number" ? props.lineNumbers : 1;
2837
+ const lineNumWidth = String(startLineNum + codeLines.length - 1).length;
2838
+ const hasHeader = showHeader ? " cm-draftly-code-block-has-header" : "";
2839
+ const hasCaption = props.caption ? " cm-draftly-code-block-has-caption" : "";
2840
+ html += `<pre class="cm-draftly-code-block${hasHeader}${hasCaption}"${props.language ? ` data-lang="${ctx.sanitize(props.language)}"` : ""}>`;
2841
+ html += `<code>`;
2842
+ codeLines.forEach((line, index) => {
2843
+ const lineNum = startLineNum + index;
2844
+ const isHighlighted = props.highlightLines?.includes(index + 1);
2845
+ const lineClasses = ["cm-draftly-code-line"];
2846
+ if (isHighlighted) lineClasses.push("cm-draftly-code-line-highlight");
2847
+ if (props.lineNumbers) lineClasses.push("cm-draftly-code-line-numbered");
2848
+ const lineAttrs = [`class="${lineClasses.join(" ")}"`];
2849
+ if (props.lineNumbers) {
2850
+ lineAttrs.push(`data-line-num="${lineNum}"`);
2851
+ lineAttrs.push(`style="--line-num-width: ${lineNumWidth}ch"`);
2852
+ }
2853
+ let lineContent = this.highlightCodeLine(line, props.language || "", ctx);
2854
+ if (props.highlightText && props.highlightText.length > 0) {
2855
+ lineContent = this.applyTextHighlights(lineContent, props.highlightText);
2856
+ }
2857
+ html += `<span ${lineAttrs.join(" ")}>${lineContent || " "}</span>`;
2858
+ });
2859
+ html += `</code></pre>`;
2860
+ if (props.caption) {
2861
+ html += `<div class="cm-draftly-code-caption">${ctx.sanitize(props.caption)}</div>`;
2862
+ }
2863
+ html += `</div>`;
2864
+ return html;
2865
+ }
2866
+ if (node.name === "CodeInfo" || node.name === "CodeText") {
2867
+ return "";
2868
+ }
2869
+ return null;
2870
+ }
2871
+ /**
2872
+ * Highlight a single line of code using the language's Lezer parser.
2873
+ * Falls back to sanitized plain text if the language is not supported.
2874
+ */
2875
+ highlightCodeLine(line, lang, ctx) {
2876
+ if (!lang || !line) {
2877
+ return ctx.sanitize(line);
2878
+ }
2879
+ const langDesc = languages.find(
2880
+ (l) => l.name.toLowerCase() === lang.toLowerCase() || l.alias && l.alias.includes(lang.toLowerCase())
2881
+ );
2882
+ if (!langDesc || !langDesc.support) {
2883
+ return ctx.sanitize(line);
2884
+ }
2885
+ try {
2886
+ const parser = langDesc.support.language.parser;
2887
+ const tree = parser.parse(line);
2888
+ let result = "";
2889
+ highlightCode(
2890
+ line,
2891
+ tree,
2892
+ classHighlighter,
2893
+ (text, classes2) => {
2894
+ if (classes2) {
2895
+ result += `<span class="${classes2}">${ctx.sanitize(text)}</span>`;
2896
+ } else {
2897
+ result += ctx.sanitize(text);
2898
+ }
2899
+ },
2900
+ () => {
2901
+ }
2902
+ // No newlines for single line
2903
+ );
2904
+ return result;
2905
+ } catch {
2906
+ return ctx.sanitize(line);
2907
+ }
2908
+ }
2909
+ /**
2910
+ * Apply text highlights (regex patterns) to already syntax-highlighted HTML.
2911
+ * Wraps matched patterns in `<mark>` elements.
2912
+ */
2913
+ applyTextHighlights(htmlContent, highlights) {
2914
+ let result = htmlContent;
2915
+ for (const highlight of highlights) {
2916
+ try {
2917
+ const regex = new RegExp(`(${highlight.pattern})`, "g");
2918
+ let matchCount = 0;
2919
+ result = result.replace(regex, (match) => {
2920
+ matchCount++;
2921
+ const shouldHighlight = !highlight.instances || highlight.instances.includes(matchCount);
2922
+ if (shouldHighlight) {
2923
+ return `<mark class="cm-draftly-code-text-highlight">${match}</mark>`;
2924
+ }
2925
+ return match;
2926
+ });
2927
+ } catch {
2928
+ }
2929
+ }
2930
+ return result;
2931
+ }
2932
+ };
2933
+ var theme10 = createTheme({
2934
+ default: {
2935
+ // Inline code
2936
+ ".cm-draftly-code-inline": {
2937
+ fontFamily: "var(--font-jetbrains-mono, monospace)",
2938
+ fontSize: "0.9rem",
2939
+ backgroundColor: "rgba(0, 0, 0, 0.05)",
2940
+ padding: "0.1rem 0.25rem",
2941
+ border: "1px solid var(--color-border)",
2942
+ borderRadius: "3px"
2943
+ },
2944
+ // Fenced code block lines
2945
+ ".cm-draftly-code-block-line": {
2946
+ "--radius": "0.375rem",
2947
+ fontFamily: "var(--font-jetbrains-mono, monospace)",
2948
+ fontSize: "0.9rem",
2949
+ backgroundColor: "rgba(0, 0, 0, 0.03)",
2950
+ padding: "0 1rem !important",
2951
+ lineHeight: "1.5",
2952
+ borderLeft: "1px solid var(--color-border)",
2953
+ borderRight: "1px solid var(--color-border)"
2954
+ },
2955
+ // First line of code block
2956
+ ".cm-draftly-code-block-line-start": {
2957
+ borderTopLeftRadius: "var(--radius)",
2958
+ borderTopRightRadius: "var(--radius)",
2959
+ position: "relative",
2960
+ overflow: "hidden",
2961
+ borderTop: "1px solid var(--color-border)",
2962
+ paddingBottom: "0.5rem !important"
2963
+ },
2964
+ // Remove top radius when header is present
2965
+ ".cm-draftly-code-block-has-header": {
2966
+ padding: "0 !important",
2967
+ paddingBottom: "0.5rem !important"
2968
+ },
2969
+ // Code block header widget
2970
+ ".cm-draftly-code-header": {
2971
+ display: "flex",
2972
+ justifyContent: "space-between",
2973
+ alignItems: "center",
2974
+ padding: "0.25rem 1rem",
2975
+ backgroundColor: "rgba(0, 0, 0, 0.06)",
2976
+ fontFamily: "var(--font-jetbrains-mono, monospace)",
2977
+ fontSize: "0.85rem"
2978
+ },
2979
+ ".cm-draftly-code-header-left": {
2980
+ display: "flex",
2981
+ alignItems: "center",
2982
+ gap: "0.5rem"
2983
+ },
2984
+ ".cm-draftly-code-header-title": {
2985
+ color: "var(--color-text, inherit)",
2986
+ fontWeight: "500"
2987
+ },
2988
+ ".cm-draftly-code-header-lang": {
2989
+ color: "#6a737d",
2990
+ opacity: "0.8"
2991
+ },
2992
+ ".cm-draftly-code-header-right": {
2993
+ display: "flex",
2994
+ alignItems: "center",
2995
+ gap: "0.5rem"
2996
+ },
2997
+ ".cm-draftly-code-copy-btn": {
2998
+ display: "flex",
2999
+ alignItems: "center",
3000
+ justifyContent: "center",
3001
+ padding: "0.25rem",
3002
+ backgroundColor: "transparent",
3003
+ border: "none",
3004
+ borderRadius: "4px",
3005
+ cursor: "pointer",
3006
+ color: "#6a737d",
3007
+ transition: "color 0.2s, background-color 0.2s"
3008
+ },
3009
+ ".cm-draftly-code-copy-btn:hover": {
3010
+ backgroundColor: "rgba(0, 0, 0, 0.1)",
3011
+ color: "var(--color-text, inherit)"
3012
+ },
3013
+ ".cm-draftly-code-copy-btn.copied": {
3014
+ color: "#22c55e"
3015
+ },
3016
+ // Caption (below code block)
3017
+ ".cm-draftly-code-block-has-caption": {
3018
+ padding: "0 !important",
3019
+ paddingTop: "0.5rem !important"
3020
+ },
3021
+ ".cm-draftly-code-caption": {
3022
+ textAlign: "center",
3023
+ fontSize: "0.85rem",
3024
+ color: "#6a737d",
3025
+ fontStyle: "italic",
3026
+ padding: "0.25rem 1rem",
3027
+ backgroundColor: "rgba(0, 0, 0, 0.06)"
3028
+ },
3029
+ // Last line of code block
3030
+ ".cm-draftly-code-block-line-end": {
3031
+ borderBottomLeftRadius: "var(--radius)",
3032
+ borderBottomRightRadius: "var(--radius)",
3033
+ borderBottom: "1px solid var(--color-border)",
3034
+ paddingTop: "0.5rem !important"
3035
+ },
3036
+ ".cm-draftly-code-block-line-end br": {
3037
+ display: "none"
3038
+ },
3039
+ // Fence markers (```)
3040
+ ".cm-draftly-code-fence": {
3041
+ color: "#6a737d",
3042
+ fontFamily: "var(--font-jetbrains-mono, monospace)"
3043
+ },
3044
+ // Line numbers
3045
+ ".cm-draftly-code-line-numbered": {
3046
+ paddingLeft: "calc(var(--line-num-width, 2ch) + 1rem) !important",
3047
+ position: "relative"
3048
+ },
3049
+ ".cm-draftly-code-line-numbered::before": {
3050
+ content: "attr(data-line-num)",
3051
+ position: "absolute",
3052
+ left: "0.5rem",
3053
+ top: "0.2rem",
3054
+ width: "var(--line-num-width, 2ch)",
3055
+ textAlign: "right",
3056
+ color: "#6a737d",
3057
+ opacity: "0.6",
3058
+ fontFamily: "var(--font-jetbrains-mono, monospace)",
3059
+ fontSize: "0.85rem",
3060
+ userSelect: "none"
3061
+ },
3062
+ // Preview: code lines (need block display for full-width highlights)
3063
+ ".cm-draftly-code-line": {
3064
+ display: "block",
3065
+ position: "relative",
3066
+ paddingLeft: "1rem",
3067
+ paddingRight: "1rem",
3068
+ lineHeight: "1.5",
3069
+ borderLeft: "3px solid transparent"
3070
+ },
3071
+ // Line highlight
3072
+ ".cm-draftly-code-line-highlight": {
3073
+ backgroundColor: "rgba(255, 220, 100, 0.2) !important",
3074
+ borderLeft: "3px solid #f0b429 !important"
3075
+ },
3076
+ // Text highlight
3077
+ ".cm-draftly-code-text-highlight": {
3078
+ backgroundColor: "rgba(255, 220, 100, 0.4)",
3079
+ borderRadius: "2px",
3080
+ padding: "0.1rem 0"
3081
+ },
3082
+ // Preview: container wrapper
3083
+ ".cm-draftly-code-container": {
3084
+ margin: "1rem 0",
3085
+ borderRadius: "var(--radius)",
3086
+ overflow: "hidden",
3087
+ border: "1px solid var(--color-border)"
3088
+ },
3089
+ // Preview: header inside container
3090
+ ".cm-draftly-code-container .cm-draftly-code-header": {
3091
+ borderRadius: "0",
3092
+ border: "none",
3093
+ borderBottom: "1px solid var(--color-border)"
3094
+ },
3095
+ // Preview: code block inside container
3096
+ ".cm-draftly-code-container .cm-draftly-code-block": {
3097
+ margin: "0",
3098
+ borderRadius: "0",
3099
+ border: "none",
3100
+ whiteSpace: "pre-wrap"
3101
+ },
3102
+ // Preview: caption inside container
3103
+ ".cm-draftly-code-container .cm-draftly-code-caption": {
3104
+ borderTop: "1px solid var(--color-border)"
3105
+ },
3106
+ // Preview: standalone code block (not in container)
3107
+ ".cm-draftly-code-block": {
3108
+ fontFamily: "var(--font-jetbrains-mono, monospace)",
3109
+ fontSize: "0.9rem",
3110
+ backgroundColor: "rgba(0, 0, 0, 0.03)",
3111
+ padding: "1rem",
3112
+ overflow: "auto",
3113
+ position: "relative",
3114
+ borderRadius: "var(--radius)",
3115
+ border: "1px solid var(--color-border)"
3116
+ },
3117
+ // Preview: code block with header (remove top radius)
3118
+ ".cm-draftly-code-block.cm-draftly-code-block-has-header": {
3119
+ borderTopLeftRadius: "0",
3120
+ borderTopRightRadius: "0",
3121
+ borderTop: "none",
3122
+ margin: "0",
3123
+ paddingTop: "0.5rem !important"
3124
+ },
3125
+ // Preview: code block with caption (remove bottom radius)
3126
+ ".cm-draftly-code-block.cm-draftly-code-block-has-caption": {
3127
+ borderBottomLeftRadius: "0",
3128
+ borderBottomRightRadius: "0",
3129
+ borderBottom: "none",
3130
+ paddingBottom: "0.5rem !important"
3131
+ }
3132
+ },
3133
+ dark: {
3134
+ ".cm-draftly-code-inline": {
3135
+ backgroundColor: "rgba(255, 255, 255, 0.1)"
3136
+ },
3137
+ ".cm-draftly-code-block-line": {
3138
+ backgroundColor: "rgba(255, 255, 255, 0.05)"
3139
+ },
3140
+ ".cm-draftly-code-fence": {
3141
+ color: "#8b949e"
3142
+ },
3143
+ ".cm-draftly-code-block": {
3144
+ backgroundColor: "rgba(255, 255, 255, 0.05)"
3145
+ },
3146
+ ".cm-draftly-code-header": {
3147
+ backgroundColor: "rgba(255, 255, 255, 0.08)"
3148
+ },
3149
+ ".cm-draftly-code-header-lang": {
3150
+ color: "#8b949e"
3151
+ },
3152
+ ".cm-draftly-code-copy-btn": {
3153
+ color: "#8b949e"
3154
+ },
3155
+ ".cm-draftly-code-copy-btn:hover": {
3156
+ backgroundColor: "rgba(255, 255, 255, 0.1)"
3157
+ },
3158
+ ".cm-draftly-code-caption": {
3159
+ backgroundColor: "rgba(255, 255, 255, 0.05)"
3160
+ },
3161
+ ".cm-draftly-code-line-numbered::before": {
3162
+ color: "#8b949e"
3163
+ },
3164
+ ".cm-draftly-code-line-highlight": {
3165
+ backgroundColor: "rgba(255, 220, 100, 0.15) !important",
3166
+ borderLeft: "3px solid #d9a520 !important"
3167
+ },
3168
+ ".cm-draftly-code-text-highlight": {
3169
+ backgroundColor: "rgba(255, 220, 100, 0.3)"
3170
+ }
3171
+ }
3172
+ });
3173
+ var quoteMarkDecorations = {
3174
+ /** Decoration for the > marker */
3175
+ "quote-mark": Decoration.replace({}),
3176
+ /** Decoration for the quote content */
3177
+ "quote-content": Decoration.mark({ class: "cm-draftly-quote-content" })
3178
+ };
3179
+ var quoteLineDecorations = {
3180
+ /** Decoration for blockquote lines */
3181
+ "quote-line": Decoration.line({ class: "cm-draftly-quote-line" })
3182
+ };
3183
+ var QuotePlugin = class extends DecorationPlugin {
3184
+ name = "quote";
3185
+ version = "1.0.0";
3186
+ decorationPriority = 10;
3187
+ requiredNodes = ["Blockquote", "QuoteMark"];
3188
+ /**
3189
+ * Constructor - calls super constructor
3190
+ */
3191
+ constructor() {
3192
+ super();
3193
+ }
3194
+ /**
3195
+ * Plugin theme
3196
+ */
3197
+ get theme() {
3198
+ return theme11;
3199
+ }
3200
+ /**
3201
+ * Build blockquote decorations by iterating the syntax tree
3202
+ */
3203
+ buildDecorations(ctx) {
3204
+ const { view, decorations } = ctx;
3205
+ const tree = syntaxTree(view.state);
3206
+ tree.iterate({
3207
+ enter: (node) => {
3208
+ const { from, to, name } = node;
3209
+ if (name !== "Blockquote") {
3210
+ return;
3211
+ }
3212
+ const startLine = view.state.doc.lineAt(from);
3213
+ const endLine = view.state.doc.lineAt(to);
3214
+ for (let lineNum = startLine.number; lineNum <= endLine.number; lineNum++) {
3215
+ const line = view.state.doc.line(lineNum);
3216
+ decorations.push(quoteLineDecorations["quote-line"].range(line.from));
3217
+ }
3218
+ decorations.push(quoteMarkDecorations["quote-content"].range(from, to));
3219
+ const cursorInNode = ctx.selectionOverlapsRange(from, to);
3220
+ if (!cursorInNode) {
3221
+ this.hideQuoteMarks(node.node, decorations, view);
3222
+ }
3223
+ }
3224
+ });
3225
+ }
3226
+ /**
3227
+ * Recursively find and hide quote marks
3228
+ */
3229
+ hideQuoteMarks(node, decorations, view) {
3230
+ let child = node.firstChild;
3231
+ while (child) {
3232
+ if (child.name === "QuoteMark") {
3233
+ const line = view.state.doc.lineAt(child.from);
3234
+ const markEnd = Math.min(child.to + 1, line.to);
3235
+ decorations.push(quoteMarkDecorations["quote-mark"].range(child.from, markEnd));
3236
+ }
3237
+ if (child.name === "Blockquote") {
3238
+ this.hideQuoteMarks(child, decorations, view);
3239
+ }
3240
+ child = child.nextSibling;
3241
+ }
3242
+ }
3243
+ renderToHTML(node, children) {
3244
+ if (node.name === "QuoteMark") {
3245
+ return "";
3246
+ }
3247
+ if (node.name !== "Blockquote") {
3248
+ return null;
3249
+ }
3250
+ return `<blockquote class="cm-draftly-quote-line"><div class="cm-draftly-quote-content">${children}</div></blockquote>
3251
+ `;
3252
+ }
3253
+ };
3254
+ var theme11 = createTheme({
3255
+ default: {
3256
+ // Line styling with left border
3257
+ ".cm-draftly-quote-line": {
3258
+ borderLeft: "3px solid currentColor",
3259
+ paddingLeft: "1em !important",
3260
+ paddingTop: "0.25em !important",
3261
+ paddingBottom: "0.25em !important",
3262
+ marginLeft: "0.25em",
3263
+ opacity: "0.85"
3264
+ },
3265
+ // Quote content styling
3266
+ ".cm-draftly-quote-content": {
3267
+ fontStyle: "italic"
3268
+ }
3269
+ }
3270
+ });
3271
+ var hrLineDecoration = Decoration.line({ class: "cm-draftly-hr-line" });
3272
+ var hrMarkDecoration = Decoration.replace({});
3273
+ var HRPlugin = class extends DecorationPlugin {
3274
+ name = "hr";
3275
+ version = "1.0.0";
3276
+ decorationPriority = 10;
3277
+ requiredNodes = ["HorizontalRule"];
3278
+ /**
3279
+ * Constructor - calls super constructor
3280
+ */
3281
+ constructor() {
3282
+ super();
3283
+ }
3284
+ /**
3285
+ * Plugin theme
3286
+ */
3287
+ get theme() {
3288
+ return theme12;
3289
+ }
3290
+ /**
3291
+ * Build horizontal rule decorations by iterating the syntax tree
3292
+ */
3293
+ buildDecorations(ctx) {
3294
+ const { view, decorations } = ctx;
3295
+ const tree = syntaxTree(view.state);
3296
+ tree.iterate({
3297
+ enter: (node) => {
3298
+ const { from, to, name } = node;
3299
+ if (name !== "HorizontalRule") {
3300
+ return;
3301
+ }
3302
+ const line = view.state.doc.lineAt(from);
3303
+ decorations.push(hrLineDecoration.range(line.from));
3304
+ const cursorInNode = ctx.selectionOverlapsRange(from, to);
3305
+ if (!cursorInNode) {
3306
+ const markEnd = Math.min(to, line.to);
3307
+ decorations.push(hrMarkDecoration.range(from, markEnd));
3308
+ }
3309
+ }
3310
+ });
3311
+ }
3312
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
3313
+ renderToHTML(node, _children) {
3314
+ if (node.name !== "HorizontalRule") {
3315
+ return null;
3316
+ }
3317
+ return `<hr class="cm-draftly-hr-line" />
3318
+ `;
3319
+ }
3320
+ };
3321
+ var theme12 = createTheme({
3322
+ default: {
3323
+ // Line styling — displays a centered horizontal line
3324
+ ".cm-draftly-hr-line": {
3325
+ display: "flex",
3326
+ alignItems: "center",
3327
+ paddingTop: "0.75em",
3328
+ paddingBottom: "0.75em",
3329
+ border: "none",
3330
+ "&::after": {
3331
+ content: '""',
3332
+ flex: "1",
3333
+ height: "2px",
3334
+ background: "currentColor",
3335
+ opacity: "0.3"
3336
+ }
3337
+ }
3338
+ }
3339
+ });
3340
+
3341
+ // src/plugins/index.ts
3342
+ var essentialPlugins = [
3343
+ new ParagraphPlugin(),
3344
+ new HeadingPlugin(),
3345
+ new InlinePlugin(),
3346
+ new LinkPlugin(),
3347
+ new ListPlugin(),
3348
+ new HTMLPlugin(),
3349
+ new ImagePlugin(),
3350
+ new MathPlugin(),
3351
+ new MermaidPlugin(),
3352
+ new CodePlugin(),
3353
+ new QuotePlugin(),
3354
+ new HRPlugin()
3355
+ ];
3356
+ var allPlugins = [...essentialPlugins];
3357
+
3358
+ export { CodePlugin, HRPlugin, HTMLPlugin, HeadingPlugin, ImagePlugin, InlinePlugin, LinkPlugin, ListPlugin, MathPlugin, MermaidPlugin, ParagraphPlugin, QuotePlugin, allPlugins, essentialPlugins };
3359
+ //# sourceMappingURL=chunk-N3WL3XPB.js.map
3360
+ //# sourceMappingURL=chunk-N3WL3XPB.js.map