mardora 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +113 -0
  3. package/dist/chunk-3OCUX4OO.js +7690 -0
  4. package/dist/chunk-3OCUX4OO.js.map +1 -0
  5. package/dist/chunk-3ZOCCFDL.cjs +74 -0
  6. package/dist/chunk-3ZOCCFDL.cjs.map +1 -0
  7. package/dist/chunk-7JOEPNEV.cjs +7740 -0
  8. package/dist/chunk-7JOEPNEV.cjs.map +1 -0
  9. package/dist/chunk-BIKZQZ6W.js +33 -0
  10. package/dist/chunk-BIKZQZ6W.js.map +1 -0
  11. package/dist/chunk-EQJESPP2.js +234 -0
  12. package/dist/chunk-EQJESPP2.js.map +1 -0
  13. package/dist/chunk-G4SE26YY.js +70 -0
  14. package/dist/chunk-G4SE26YY.js.map +1 -0
  15. package/dist/chunk-KNDWF2DP.cjs +35 -0
  16. package/dist/chunk-KNDWF2DP.cjs.map +1 -0
  17. package/dist/chunk-MLBEBFHB.cjs +2971 -0
  18. package/dist/chunk-MLBEBFHB.cjs.map +1 -0
  19. package/dist/chunk-P7JFCYU3.js +905 -0
  20. package/dist/chunk-P7JFCYU3.js.map +1 -0
  21. package/dist/chunk-SWFUKJDO.cjs +243 -0
  22. package/dist/chunk-SWFUKJDO.cjs.map +1 -0
  23. package/dist/chunk-WFVCG4LD.cjs +926 -0
  24. package/dist/chunk-WFVCG4LD.cjs.map +1 -0
  25. package/dist/chunk-XL6WFGJT.js +2901 -0
  26. package/dist/chunk-XL6WFGJT.js.map +1 -0
  27. package/dist/editor/index.cjs +277 -0
  28. package/dist/editor/index.cjs.map +1 -0
  29. package/dist/editor/index.d.cts +186 -0
  30. package/dist/editor/index.d.ts +186 -0
  31. package/dist/editor/index.js +4 -0
  32. package/dist/editor/index.js.map +1 -0
  33. package/dist/index.cjs +405 -0
  34. package/dist/index.cjs.map +1 -0
  35. package/dist/index.d.cts +13 -0
  36. package/dist/index.d.ts +13 -0
  37. package/dist/index.js +8 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/lib/index.cjs +12 -0
  40. package/dist/lib/index.cjs.map +1 -0
  41. package/dist/lib/index.d.cts +16 -0
  42. package/dist/lib/index.d.ts +16 -0
  43. package/dist/lib/index.js +3 -0
  44. package/dist/lib/index.js.map +1 -0
  45. package/dist/mardora-DCwjomil.d.cts +640 -0
  46. package/dist/mardora-DCwjomil.d.ts +640 -0
  47. package/dist/plugins/index.cjs +104 -0
  48. package/dist/plugins/index.cjs.map +1 -0
  49. package/dist/plugins/index.d.cts +740 -0
  50. package/dist/plugins/index.d.ts +740 -0
  51. package/dist/plugins/index.js +7 -0
  52. package/dist/plugins/index.js.map +1 -0
  53. package/dist/preview/index.cjs +38 -0
  54. package/dist/preview/index.cjs.map +1 -0
  55. package/dist/preview/index.d.cts +101 -0
  56. package/dist/preview/index.d.ts +101 -0
  57. package/dist/preview/index.js +5 -0
  58. package/dist/preview/index.js.map +1 -0
  59. package/dist/types-NBsaxl4d.d.cts +71 -0
  60. package/dist/types-Pw2SWWAR.d.ts +71 -0
  61. package/package.json +92 -0
  62. package/src/editor/attachments/extension.ts +181 -0
  63. package/src/editor/attachments/format.ts +63 -0
  64. package/src/editor/attachments/index.ts +3 -0
  65. package/src/editor/attachments/types.ts +37 -0
  66. package/src/editor/heading-fold/config.ts +25 -0
  67. package/src/editor/heading-fold/extension.ts +268 -0
  68. package/src/editor/heading-fold/extract.ts +88 -0
  69. package/src/editor/heading-fold/index.ts +5 -0
  70. package/src/editor/heading-fold/theme.ts +85 -0
  71. package/src/editor/heading-fold/types.ts +24 -0
  72. package/src/editor/i18n.ts +13 -0
  73. package/src/editor/icons/index.ts +367 -0
  74. package/src/editor/index.ts +16 -0
  75. package/src/editor/mardora.ts +257 -0
  76. package/src/editor/media-lightbox-theme.ts +146 -0
  77. package/src/editor/media-lightbox.ts +125 -0
  78. package/src/editor/plugin.ts +294 -0
  79. package/src/editor/selection-toolbar/activation.ts +123 -0
  80. package/src/editor/selection-toolbar/commands.ts +279 -0
  81. package/src/editor/selection-toolbar/extension.ts +564 -0
  82. package/src/editor/selection-toolbar/i18n.ts +164 -0
  83. package/src/editor/selection-toolbar/index.ts +7 -0
  84. package/src/editor/selection-toolbar/menu.ts +252 -0
  85. package/src/editor/selection-toolbar/position.ts +43 -0
  86. package/src/editor/selection-toolbar/theme.ts +195 -0
  87. package/src/editor/selection-toolbar/types.ts +155 -0
  88. package/src/editor/slash/default-commands.ts +190 -0
  89. package/src/editor/slash/extension.ts +319 -0
  90. package/src/editor/slash/index.ts +7 -0
  91. package/src/editor/slash/insertions.ts +26 -0
  92. package/src/editor/slash/menu.ts +123 -0
  93. package/src/editor/slash/position.ts +61 -0
  94. package/src/editor/slash/query.ts +33 -0
  95. package/src/editor/slash/theme.ts +113 -0
  96. package/src/editor/slash/types.ts +40 -0
  97. package/src/editor/table-of-contents/extension.ts +202 -0
  98. package/src/editor/table-of-contents/extract.ts +53 -0
  99. package/src/editor/table-of-contents/index.ts +7 -0
  100. package/src/editor/table-of-contents/panel.ts +83 -0
  101. package/src/editor/table-of-contents/slug.ts +50 -0
  102. package/src/editor/table-of-contents/storage.ts +35 -0
  103. package/src/editor/table-of-contents/theme.ts +153 -0
  104. package/src/editor/table-of-contents/types.ts +44 -0
  105. package/src/editor/theme.ts +72 -0
  106. package/src/editor/utils.ts +176 -0
  107. package/src/editor/view-plugin.ts +189 -0
  108. package/src/index.ts +5 -0
  109. package/src/lib/index.ts +2 -0
  110. package/src/lib/input-handler.ts +47 -0
  111. package/src/plugins/code-plugin.theme.ts +545 -0
  112. package/src/plugins/code-plugin.ts +1892 -0
  113. package/src/plugins/emoji-plugin.ts +140 -0
  114. package/src/plugins/heading-plugin.ts +194 -0
  115. package/src/plugins/hr-plugin.ts +102 -0
  116. package/src/plugins/html-plugin.ts +353 -0
  117. package/src/plugins/image-plugin.ts +806 -0
  118. package/src/plugins/index.ts +71 -0
  119. package/src/plugins/inline-plugin.ts +311 -0
  120. package/src/plugins/link-plugin.ts +509 -0
  121. package/src/plugins/list-plugin.ts +492 -0
  122. package/src/plugins/math-plugin.ts +526 -0
  123. package/src/plugins/mermaid-plugin.ts +513 -0
  124. package/src/plugins/paragraph-plugin.ts +38 -0
  125. package/src/plugins/quote-plugin.ts +733 -0
  126. package/src/plugins/table-controls-theme.ts +126 -0
  127. package/src/plugins/table-controls.ts +423 -0
  128. package/src/plugins/table-model.ts +661 -0
  129. package/src/plugins/table-plugin.ts +2111 -0
  130. package/src/preview/context.ts +45 -0
  131. package/src/preview/css-generator.ts +64 -0
  132. package/src/preview/default-renderers.ts +29 -0
  133. package/src/preview/index.ts +29 -0
  134. package/src/preview/preview.ts +41 -0
  135. package/src/preview/renderer.ts +184 -0
  136. package/src/preview/syntax-theme.ts +112 -0
  137. package/src/preview/toc.ts +23 -0
  138. package/src/preview/types.ts +89 -0
@@ -0,0 +1,733 @@
1
+ import { Decoration, EditorView, WidgetType } from "@codemirror/view";
2
+ import { EditorState, Extension, TransactionSpec } from "@codemirror/state";
3
+ import { syntaxTree } from "@codemirror/language";
4
+ import { DecorationContext, DecorationPlugin } from "../editor/plugin";
5
+ import { createTheme } from "../editor";
6
+ import { SyntaxNode } from "@lezer/common";
7
+ import { createMardoraIcon, type MardoraIconName } from "../editor/icons";
8
+
9
+ export type CalloutLabel = "NOTE" | "TIP" | "IMPORTANT" | "WARNING" | "CAUTION";
10
+
11
+ type CalloutInfo = {
12
+ label: CalloutLabel;
13
+ type: Lowercase<CalloutLabel>;
14
+ };
15
+
16
+ export type CalloutTitleInputTarget = {
17
+ anchor: number;
18
+ changes?: {
19
+ from: number;
20
+ insert: string;
21
+ };
22
+ };
23
+
24
+ export type CalloutTypeChange = {
25
+ from: number;
26
+ to: number;
27
+ insert: CalloutLabel;
28
+ };
29
+
30
+ const calloutLabels: readonly CalloutLabel[] = ["NOTE", "TIP", "IMPORTANT", "WARNING", "CAUTION"];
31
+ const calloutIconMap: Record<CalloutLabel, MardoraIconName> = {
32
+ NOTE: "info",
33
+ TIP: "lightbulb",
34
+ IMPORTANT: "badge-alert",
35
+ WARNING: "triangle-alert",
36
+ CAUTION: "octagon-alert",
37
+ };
38
+ const calloutIconMarkupMap: Record<CalloutLabel, string> = {
39
+ NOTE: '<circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4"></path><path d="M12 8h.01"></path>',
40
+ TIP: '<path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"></path><path d="M9 18h6"></path><path d="M10 22h4"></path>',
41
+ IMPORTANT:
42
+ '<path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"></path><line x1="12" x2="12" y1="8" y2="12"></line><line x1="12" x2="12.01" y1="16" y2="16"></line>',
43
+ WARNING:
44
+ '<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path>',
45
+ CAUTION:
46
+ '<path d="M12 16h.01"></path><path d="M12 8v4"></path><path d="M15.312 2a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586l-4.688-4.688A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2z"></path>',
47
+ };
48
+ const calloutMarkerPattern = /^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]$/i;
49
+ const calloutMarkerSearchPattern = /\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]/i;
50
+ const quotePrefixPattern = /^ {0,3}>\s?/;
51
+
52
+ function stripQuoteMarker(line: string): string {
53
+ return line.replace(quotePrefixPattern, "");
54
+ }
55
+
56
+ export function parseCalloutInfo(markdown: string): CalloutInfo | null {
57
+ const firstLine = markdown.split(/\r?\n/, 1)[0] ?? "";
58
+ const match = stripQuoteMarker(firstLine).trim().match(calloutMarkerPattern);
59
+
60
+ if (!match?.[1]) {
61
+ return null;
62
+ }
63
+
64
+ const label = match[1].toUpperCase() as CalloutLabel;
65
+ return { label, type: label.toLowerCase() as Lowercase<CalloutLabel> };
66
+ }
67
+
68
+ function removeRenderedCalloutMarker(children: string, label: CalloutLabel): string {
69
+ const markerPattern = new RegExp(`^(\\s*(?:<p\\b[^>]*>)?\\s*)\\[!${label}\\]\\s*(?:\\r?\\n)?\\s*`, "i");
70
+ return children.replace(markerPattern, "$1");
71
+ }
72
+
73
+ function renderCalloutIconHTML(label: CalloutLabel): string {
74
+ return `<svg class="cm-mardora-callout-title-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">${calloutIconMarkupMap[label]}</svg>`;
75
+ }
76
+
77
+ function getQuotePrefixLength(line: string): number | null {
78
+ const match = line.match(quotePrefixPattern);
79
+ return match ? match[0].length : null;
80
+ }
81
+
82
+ export function resolveCalloutTitleInputTarget(state: EditorState, pos: number): CalloutTitleInputTarget | null {
83
+ const titleLine = state.doc.lineAt(pos);
84
+
85
+ if (!parseCalloutInfo(titleLine.text)) {
86
+ return null;
87
+ }
88
+
89
+ const bodyLineNumber = titleLine.number + 1;
90
+ if (bodyLineNumber <= state.doc.lines) {
91
+ const bodyLine = state.doc.line(bodyLineNumber);
92
+ const quotePrefixLength = getQuotePrefixLength(bodyLine.text);
93
+
94
+ if (quotePrefixLength !== null) {
95
+ return {
96
+ anchor: bodyLine.from + quotePrefixLength,
97
+ };
98
+ }
99
+ }
100
+
101
+ const insert = "\n> ";
102
+ return {
103
+ anchor: titleLine.to + insert.length,
104
+ changes: {
105
+ from: titleLine.to,
106
+ insert,
107
+ },
108
+ };
109
+ }
110
+
111
+ export function resolveCalloutTypeChange(
112
+ state: EditorState,
113
+ pos: number,
114
+ nextLabel: CalloutLabel
115
+ ): CalloutTypeChange | null {
116
+ const titleLine = state.doc.lineAt(pos);
117
+ const callout = parseCalloutInfo(titleLine.text);
118
+ if (!callout || callout.label === nextLabel) {
119
+ return null;
120
+ }
121
+
122
+ const markerMatch = titleLine.text.match(calloutMarkerSearchPattern);
123
+ if (!markerMatch?.[1] || markerMatch.index === undefined) {
124
+ return null;
125
+ }
126
+
127
+ const typeFrom = titleLine.from + markerMatch.index + 2;
128
+ return {
129
+ from: typeFrom,
130
+ to: typeFrom + markerMatch[1].length,
131
+ insert: nextLabel,
132
+ };
133
+ }
134
+
135
+ class CalloutTitleWidget extends WidgetType {
136
+ constructor(
137
+ readonly info: CalloutInfo,
138
+ readonly pos: number
139
+ ) {
140
+ super();
141
+ }
142
+
143
+ override eq(other: CalloutTitleWidget): boolean {
144
+ return other.info.label === this.info.label && other.pos === this.pos;
145
+ }
146
+
147
+ toDOM(): HTMLElement {
148
+ const button = document.createElement("button");
149
+ button.type = "button";
150
+ button.className = "cm-mardora-callout-title-button";
151
+ button.dataset.calloutLabel = this.info.label;
152
+ button.dataset.calloutPos = String(this.pos);
153
+ button.setAttribute("aria-haspopup", "menu");
154
+ button.setAttribute("aria-label", `Switch ${this.info.label} callout type`);
155
+
156
+ const icon = createMardoraIcon(calloutIconMap[this.info.label]);
157
+ if (icon) {
158
+ icon.classList.add("cm-mardora-callout-title-icon");
159
+ button.appendChild(icon);
160
+ }
161
+
162
+ const label = document.createElement("span");
163
+ label.textContent = this.info.label;
164
+ button.appendChild(label);
165
+
166
+ return button;
167
+ }
168
+
169
+ override ignoreEvent(event: Event): boolean {
170
+ return event.type !== "mousedown" && event.type !== "click";
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Mark decorations for blockquote elements
176
+ */
177
+ const quoteMarkDecorations = {
178
+ /** Decoration for the > marker */
179
+ "quote-mark": Decoration.replace({}),
180
+ /** Decoration for the quote content */
181
+ "quote-content": Decoration.mark({ class: "cm-mardora-quote-content" }),
182
+ /** Decoration for callout content */
183
+ "callout-content": Decoration.mark({ class: "cm-mardora-callout-content" }),
184
+ };
185
+
186
+ function calloutMarkerDecoration(info: CalloutInfo, pos: number): Decoration {
187
+ return Decoration.replace({ widget: new CalloutTitleWidget(info, pos) });
188
+ }
189
+
190
+ /**
191
+ * Line decorations for blockquote lines
192
+ */
193
+ const quoteLineDecorations = {
194
+ /** Decoration for blockquote lines */
195
+ "quote-line": Decoration.line({ class: "cm-mardora-quote-line" }),
196
+ };
197
+
198
+ function calloutLineDecoration(info: CalloutInfo, first: boolean): Decoration {
199
+ const spec: { class: string; attributes?: { [key: string]: string } } = {
200
+ class: ["cm-mardora-callout-line", `cm-mardora-callout-${info.type}`, first ? "cm-mardora-callout-title-line" : ""]
201
+ .filter(Boolean)
202
+ .join(" "),
203
+ };
204
+
205
+ return Decoration.line(spec);
206
+ }
207
+
208
+ function getQuoteMarkReplacementEnd(lineText: string, nodeTo: number, lineFrom: number): number {
209
+ const restOfLine = lineText.slice(nodeTo - lineFrom);
210
+ if (restOfLine.trim().length === 0) {
211
+ return nodeTo;
212
+ }
213
+
214
+ return Math.min(nodeTo + 1, lineFrom + lineText.length);
215
+ }
216
+
217
+ /**
218
+ * QuotePlugin - Decorates markdown blockquotes
219
+ *
220
+ * Adds visual styling to blockquotes (> prefixed lines)
221
+ * - Line decorations for indicating quote blocks with a left border
222
+ * - Mark decorations for quote content
223
+ * - Hides > markers when cursor is not in the blockquote
224
+ */
225
+ export class QuotePlugin extends DecorationPlugin {
226
+ readonly name = "quote";
227
+ readonly version = "1.0.0";
228
+ override decorationPriority = 10;
229
+ override readonly requiredNodes = ["Blockquote", "QuoteMark"] as const;
230
+ private calloutTypeMenu: HTMLElement | null = null;
231
+ private calloutTypeMenuDocument: Document | null = null;
232
+
233
+ /**
234
+ * Constructor - calls super constructor
235
+ */
236
+ constructor() {
237
+ super();
238
+ }
239
+
240
+ /**
241
+ * Plugin theme
242
+ */
243
+ override get theme() {
244
+ return theme;
245
+ }
246
+
247
+ override getExtensions(): Extension[] {
248
+ return [
249
+ EditorView.domEventHandlers({
250
+ keydown: (event) => this.handleKeyDown(event),
251
+ mousedown: (event, view) => this.handleCalloutTitleMouseDown(event, view),
252
+ }),
253
+ ];
254
+ }
255
+
256
+ override onViewUpdate(update: import("@codemirror/view").ViewUpdate): void {
257
+ if (update.docChanged) {
258
+ this.closeCalloutTypeMenu();
259
+ }
260
+ }
261
+
262
+ override onUnregister(): void {
263
+ this.closeCalloutTypeMenu();
264
+ super.onUnregister();
265
+ }
266
+
267
+ private handleKeyDown(event: KeyboardEvent): boolean {
268
+ if (event.key !== "Escape" || !this.calloutTypeMenu) {
269
+ return false;
270
+ }
271
+
272
+ this.closeCalloutTypeMenu();
273
+ event.preventDefault();
274
+ return true;
275
+ }
276
+
277
+ private handleCalloutTitleMouseDown(event: MouseEvent, view: EditorView): boolean {
278
+ if (event.button !== 0) {
279
+ return false;
280
+ }
281
+
282
+ const target = event.target;
283
+ if (!(target instanceof Element)) {
284
+ return false;
285
+ }
286
+
287
+ const titleButton = target.closest(".cm-mardora-callout-title-button");
288
+ if (titleButton instanceof HTMLElement && view.dom.contains(titleButton)) {
289
+ const pos = Number(titleButton.dataset.calloutPos);
290
+ if (!Number.isFinite(pos)) {
291
+ return false;
292
+ }
293
+
294
+ event.preventDefault();
295
+ event.stopPropagation();
296
+ this.openCalloutTypeMenu(view, titleButton, pos);
297
+ return true;
298
+ }
299
+
300
+ const lineElement = target.closest(".cm-mardora-callout-title-line");
301
+ if (!lineElement || !view.dom.contains(lineElement)) {
302
+ return false;
303
+ }
304
+
305
+ let pos: number | null = null;
306
+ try {
307
+ pos = view.posAtDOM(lineElement, 0);
308
+ } catch {
309
+ pos = null;
310
+ }
311
+
312
+ pos ??= view.posAtCoords({ x: event.clientX, y: event.clientY });
313
+ if (pos === null) {
314
+ return false;
315
+ }
316
+
317
+ const inputTarget = resolveCalloutTitleInputTarget(view.state, pos);
318
+ if (!inputTarget) {
319
+ return false;
320
+ }
321
+
322
+ event.preventDefault();
323
+ event.stopPropagation();
324
+
325
+ const transaction: TransactionSpec = {
326
+ selection: { anchor: inputTarget.anchor },
327
+ scrollIntoView: true,
328
+ };
329
+
330
+ if (inputTarget.changes) {
331
+ transaction.changes = inputTarget.changes;
332
+ }
333
+
334
+ view.dispatch(transaction);
335
+ view.focus();
336
+ return true;
337
+ }
338
+
339
+ private openCalloutTypeMenu(view: EditorView, button: HTMLElement, pos: number): void {
340
+ this.closeCalloutTypeMenu();
341
+
342
+ const current = parseCalloutInfo(view.state.doc.lineAt(pos).text);
343
+ if (!current) {
344
+ return;
345
+ }
346
+
347
+ const doc = view.dom.ownerDocument;
348
+ const menu = doc.createElement("div");
349
+ menu.className = "cm-mardora-callout-type-menu";
350
+ menu.setAttribute("role", "menu");
351
+ menu.setAttribute("aria-label", "Switch callout type");
352
+
353
+ for (const label of calloutLabels) {
354
+ const item = doc.createElement("button");
355
+ item.type = "button";
356
+ item.className = [
357
+ "cm-mardora-callout-type-menu-item",
358
+ `cm-mardora-callout-type-menu-item-${label.toLowerCase()}`,
359
+ label === current.label ? "cm-mardora-callout-type-menu-item-active" : "",
360
+ ]
361
+ .filter(Boolean)
362
+ .join(" ");
363
+ item.setAttribute("role", "menuitem");
364
+ item.dataset.calloutLabel = label;
365
+ if (label === current.label) {
366
+ item.setAttribute("aria-current", "true");
367
+ }
368
+
369
+ const icon = createMardoraIcon(calloutIconMap[label]);
370
+ if (icon) {
371
+ icon.classList.add("cm-mardora-callout-type-menu-icon");
372
+ item.appendChild(icon);
373
+ }
374
+
375
+ const text = doc.createElement("span");
376
+ text.textContent = label;
377
+ item.appendChild(text);
378
+
379
+ item.addEventListener("mousedown", (event) => {
380
+ event.preventDefault();
381
+ event.stopPropagation();
382
+
383
+ const change = resolveCalloutTypeChange(view.state, pos, label);
384
+ if (change) {
385
+ view.dispatch({
386
+ changes: change,
387
+ selection: { anchor: change.from + change.insert.length },
388
+ scrollIntoView: true,
389
+ });
390
+ }
391
+ this.closeCalloutTypeMenu();
392
+ view.focus();
393
+ });
394
+
395
+ menu.appendChild(item);
396
+ }
397
+
398
+ view.dom.appendChild(menu);
399
+ this.calloutTypeMenu = menu;
400
+ this.calloutTypeMenuDocument = doc;
401
+ doc.addEventListener("mousedown", this.handleDocumentMouseDown, true);
402
+ this.positionCalloutTypeMenu(button, menu);
403
+ }
404
+
405
+ private positionCalloutTypeMenu(button: HTMLElement, menu: HTMLElement): void {
406
+ const rect = button.getBoundingClientRect();
407
+ const win = button.ownerDocument.defaultView;
408
+ const menuWidth = 176;
409
+ const viewportWidth = win?.innerWidth ?? rect.left + menuWidth;
410
+ const left = Math.max(8, Math.min(rect.left, viewportWidth - menuWidth - 8));
411
+ menu.style.left = `${left}px`;
412
+ menu.style.top = `${rect.bottom + 6}px`;
413
+ }
414
+
415
+ private readonly handleDocumentMouseDown = (event: MouseEvent): void => {
416
+ const target = event.target;
417
+ if (
418
+ target instanceof Element &&
419
+ (target.closest(".cm-mardora-callout-type-menu") || target.closest(".cm-mardora-callout-title-button"))
420
+ ) {
421
+ return;
422
+ }
423
+
424
+ this.closeCalloutTypeMenu();
425
+ };
426
+
427
+ private closeCalloutTypeMenu(): void {
428
+ if (this.calloutTypeMenuDocument) {
429
+ this.calloutTypeMenuDocument.removeEventListener("mousedown", this.handleDocumentMouseDown, true);
430
+ this.calloutTypeMenuDocument = null;
431
+ }
432
+ this.calloutTypeMenu?.remove();
433
+ this.calloutTypeMenu = null;
434
+ }
435
+
436
+ /**
437
+ * Build blockquote decorations by iterating the syntax tree
438
+ */
439
+ buildDecorations(ctx: DecorationContext): void {
440
+ const { view, decorations } = ctx;
441
+ const tree = syntaxTree(view.state);
442
+
443
+ tree.iterate({
444
+ enter: (node) => {
445
+ const { from, to, name } = node;
446
+
447
+ if (name !== "Blockquote") {
448
+ return;
449
+ }
450
+
451
+ const raw = view.state.sliceDoc(from, to);
452
+ const callout = parseCalloutInfo(raw);
453
+
454
+ // Process each line within the blockquote
455
+ const startLine = view.state.doc.lineAt(from);
456
+ const endLine = view.state.doc.lineAt(to);
457
+
458
+ for (let lineNum = startLine.number; lineNum <= endLine.number; lineNum++) {
459
+ const line = view.state.doc.line(lineNum);
460
+
461
+ // Add line decoration for the blockquote border
462
+ decorations.push(
463
+ callout
464
+ ? calloutLineDecoration(callout, lineNum === startLine.number).range(line.from)
465
+ : quoteLineDecorations["quote-line"].range(line.from)
466
+ );
467
+ }
468
+
469
+ // Add mark decoration for the entire blockquote content
470
+ decorations.push(
471
+ (callout ? quoteMarkDecorations["callout-content"] : quoteMarkDecorations["quote-content"]).range(from, to)
472
+ );
473
+
474
+ // Find all QuoteMark children (> symbols)
475
+ this.hideQuoteMarks(from, to, decorations, view);
476
+ if (callout) {
477
+ this.hideCalloutMarker(startLine.from, startLine.to, callout, decorations, view);
478
+ }
479
+ },
480
+ });
481
+ }
482
+
483
+ private hideCalloutMarker(
484
+ from: number,
485
+ to: number,
486
+ info: CalloutInfo,
487
+ decorations: import("@codemirror/state").Range<Decoration>[],
488
+ view: import("@codemirror/view").EditorView
489
+ ): void {
490
+ const lineText = view.state.sliceDoc(from, to);
491
+ const markerMatch = lineText.match(calloutMarkerSearchPattern);
492
+
493
+ if (markerMatch?.index === undefined || !markerMatch[0]) {
494
+ return;
495
+ }
496
+
497
+ const markerFrom = from + markerMatch.index;
498
+ decorations.push(calloutMarkerDecoration(info, markerFrom).range(markerFrom, markerFrom + markerMatch[0].length));
499
+ }
500
+
501
+ /**
502
+ * Find and hide quote marks in every quoted line.
503
+ */
504
+ private hideQuoteMarks(
505
+ from: number,
506
+ to: number,
507
+ decorations: import("@codemirror/state").Range<Decoration>[],
508
+ view: import("@codemirror/view").EditorView
509
+ ): void {
510
+ syntaxTree(view.state).iterate({
511
+ from,
512
+ to,
513
+ enter: (node) => {
514
+ if (node.name !== "QuoteMark") {
515
+ return;
516
+ }
517
+
518
+ // Clamp to line end so replace decoration never spans a newline.
519
+ const line = view.state.doc.lineAt(node.from);
520
+ const markEnd = getQuoteMarkReplacementEnd(line.text, node.to, line.from);
521
+ decorations.push(quoteMarkDecorations["quote-mark"].range(node.from, markEnd));
522
+ },
523
+ });
524
+ }
525
+
526
+ override renderToHTML(
527
+ node: SyntaxNode,
528
+ children: string,
529
+ ctx: { sliceDoc(from: number, to: number): string }
530
+ ): string | null {
531
+ if (node.name === "QuoteMark") {
532
+ return "";
533
+ }
534
+
535
+ if (node.name !== "Blockquote") {
536
+ return null;
537
+ }
538
+
539
+ const callout = parseCalloutInfo(ctx.sliceDoc(node.from, node.to));
540
+ if (callout) {
541
+ const content = removeRenderedCalloutMarker(children, callout.label);
542
+ return `<blockquote class="cm-mardora-callout cm-mardora-callout-${callout.type}"><div class="cm-mardora-callout-title">${renderCalloutIconHTML(callout.label)}<span>${callout.label}</span></div><div class="cm-mardora-callout-content">${content}</div></blockquote>\n`;
543
+ }
544
+
545
+ return `<blockquote class="cm-mardora-quote-line"><div class="cm-mardora-quote-content">${children}</div></blockquote>\n`;
546
+ }
547
+ }
548
+
549
+ const theme = createTheme({
550
+ default: {
551
+ // Line styling with left border
552
+ ".cm-mardora-quote-line": {
553
+ borderLeft: "3px solid currentColor",
554
+ paddingLeft: "1em !important",
555
+ paddingTop: "0.25em !important",
556
+ paddingBottom: "0.25em !important",
557
+ marginLeft: "0.25em",
558
+ minHeight: "1.6em",
559
+ boxSizing: "content-box",
560
+ opacity: "0.85",
561
+ },
562
+
563
+ // Quote content styling
564
+ ".cm-mardora-quote-content": {
565
+ fontStyle: "italic",
566
+ },
567
+
568
+ ".cm-mardora-callout-line": {
569
+ borderLeft: "4px solid var(--mardora-callout-color)",
570
+ paddingLeft: "1em !important",
571
+ paddingTop: "0.25em !important",
572
+ paddingBottom: "0.25em !important",
573
+ marginLeft: "0.25em",
574
+ minHeight: "1.6em",
575
+ backgroundColor: "var(--mardora-callout-bg)",
576
+ },
577
+
578
+ ".cm-mardora-callout-title-button": {
579
+ appearance: "none",
580
+ border: "0",
581
+ backgroundColor: "transparent",
582
+ borderRadius: "4px",
583
+ color: "var(--mardora-callout-color)",
584
+ cursor: "pointer",
585
+ display: "inline-flex",
586
+ alignItems: "center",
587
+ gap: "0.35em",
588
+ font: "inherit",
589
+ fontWeight: "700",
590
+ fontStyle: "normal",
591
+ lineHeight: "1.2",
592
+ padding: "0.05em 0.2em",
593
+ },
594
+
595
+ ".cm-mardora-callout-title-button:hover": {
596
+ backgroundColor: "color-mix(in srgb, var(--mardora-callout-color) 10%, transparent)",
597
+ },
598
+
599
+ ".cm-mardora-callout-title-button:focus-visible": {
600
+ outline: "2px solid var(--mardora-callout-color)",
601
+ outlineOffset: "2px",
602
+ },
603
+
604
+ ".cm-mardora-callout-title-icon": {
605
+ width: "0.9em",
606
+ height: "0.9em",
607
+ flex: "0 0 auto",
608
+ },
609
+
610
+ ".cm-mardora-callout-type-menu": {
611
+ position: "fixed",
612
+ zIndex: "2000",
613
+ minWidth: "176px",
614
+ display: "flex",
615
+ flexDirection: "column",
616
+ gap: "2px",
617
+ padding: "6px",
618
+ border: "1px solid var(--mardora-border-color, #d8dee8)",
619
+ borderRadius: "8px",
620
+ backgroundColor: "var(--mardora-bg-primary, #ffffff)",
621
+ boxShadow: "0 10px 28px rgba(15, 23, 42, 0.14)",
622
+ },
623
+
624
+ ".cm-mardora-callout-type-menu-item": {
625
+ appearance: "none",
626
+ border: "0",
627
+ backgroundColor: "transparent",
628
+ borderRadius: "6px",
629
+ color: "var(--mardora-text-primary, #24292f)",
630
+ cursor: "pointer",
631
+ display: "flex",
632
+ alignItems: "center",
633
+ gap: "0.5em",
634
+ font: "inherit",
635
+ fontSize: "0.875em",
636
+ fontWeight: "600",
637
+ lineHeight: "1.4",
638
+ padding: "0.45em 0.55em",
639
+ textAlign: "left",
640
+ width: "100%",
641
+ },
642
+
643
+ ".cm-mardora-callout-type-menu-item:hover": {
644
+ backgroundColor: "var(--mardora-bg-secondary, #f6f8fa)",
645
+ },
646
+
647
+ ".cm-mardora-callout-type-menu-item-active": {
648
+ backgroundColor: "var(--mardora-bg-secondary, #f6f8fa)",
649
+ color: "var(--mardora-callout-option-color)",
650
+ },
651
+
652
+ ".cm-mardora-callout-type-menu-icon": {
653
+ width: "1em",
654
+ height: "1em",
655
+ flex: "0 0 auto",
656
+ color: "var(--mardora-callout-option-color)",
657
+ },
658
+
659
+ ".cm-mardora-callout-type-menu-item-note": {
660
+ "--mardora-callout-option-color": "#0969da",
661
+ },
662
+
663
+ ".cm-mardora-callout-type-menu-item-tip": {
664
+ "--mardora-callout-option-color": "#1a7f37",
665
+ },
666
+
667
+ ".cm-mardora-callout-type-menu-item-important": {
668
+ "--mardora-callout-option-color": "#8250df",
669
+ },
670
+
671
+ ".cm-mardora-callout-type-menu-item-warning": {
672
+ "--mardora-callout-option-color": "#9a6700",
673
+ },
674
+
675
+ ".cm-mardora-callout-type-menu-item-caution": {
676
+ "--mardora-callout-option-color": "#cf222e",
677
+ },
678
+
679
+ ".cm-mardora-callout": {
680
+ borderLeft: "4px solid var(--mardora-callout-color)",
681
+ borderRadius: "0",
682
+ padding: "0.25em 1em",
683
+ margin: "1em 0",
684
+ backgroundColor: "var(--mardora-callout-bg)",
685
+ },
686
+
687
+ ".cm-mardora-callout-title": {
688
+ color: "var(--mardora-callout-color)",
689
+ display: "inline-flex",
690
+ alignItems: "center",
691
+ gap: "0.35em",
692
+ fontWeight: "700",
693
+ fontStyle: "normal",
694
+ lineHeight: "1.2",
695
+ marginBottom: "0.75em",
696
+ padding: "0.05em 0.2em",
697
+ },
698
+
699
+ ".cm-mardora-callout-content": {
700
+ fontStyle: "normal",
701
+ },
702
+
703
+ ".cm-mardora-callout-content p": {
704
+ marginTop: "0",
705
+ marginBottom: "0",
706
+ },
707
+
708
+ ".cm-mardora-callout-note": {
709
+ "--mardora-callout-color": "#0969da",
710
+ "--mardora-callout-bg": "rgba(9, 105, 218, 0.08)",
711
+ },
712
+
713
+ ".cm-mardora-callout-tip": {
714
+ "--mardora-callout-color": "#1a7f37",
715
+ "--mardora-callout-bg": "rgba(26, 127, 55, 0.08)",
716
+ },
717
+
718
+ ".cm-mardora-callout-important": {
719
+ "--mardora-callout-color": "#8250df",
720
+ "--mardora-callout-bg": "rgba(130, 80, 223, 0.08)",
721
+ },
722
+
723
+ ".cm-mardora-callout-warning": {
724
+ "--mardora-callout-color": "#9a6700",
725
+ "--mardora-callout-bg": "rgba(154, 103, 0, 0.1)",
726
+ },
727
+
728
+ ".cm-mardora-callout-caution": {
729
+ "--mardora-callout-color": "#cf222e",
730
+ "--mardora-callout-bg": "rgba(207, 34, 46, 0.08)",
731
+ },
732
+ },
733
+ });