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,513 @@
1
+ import { Decoration, EditorView, WidgetType } from "@codemirror/view";
2
+ import { syntaxTree } from "@codemirror/language";
3
+ import { DecorationContext, DecorationPlugin } from "../editor/plugin";
4
+ import { createTheme, ThemeEnum } from "../editor";
5
+ import { createMediaPreviewButton } from "../editor/media-lightbox";
6
+ import { mediaLightboxTheme } from "../editor/media-lightbox-theme";
7
+ import { SyntaxNode } from "@lezer/common";
8
+ import { tags } from "@lezer/highlight";
9
+ import type { MarkdownConfig, BlockParser, Line, BlockContext } from "@lezer/markdown";
10
+ import mermaid from "mermaid";
11
+
12
+ /**
13
+ * Initialize mermaid with default configuration
14
+ */
15
+ mermaid.initialize({
16
+ startOnLoad: false,
17
+ theme: "default",
18
+ suppressErrorRendering: true,
19
+ });
20
+
21
+ /**
22
+ * Render a mermaid diagram definition to SVG
23
+ */
24
+ let mermaidCounter = 0;
25
+ async function renderMermaid(
26
+ definition: string,
27
+ options: Record<string, string> = {},
28
+ defaultTheme: string = "default"
29
+ ): Promise<{ svg: string; error: string | null }> {
30
+ try {
31
+ const id = `mardora-mermaid-${mermaidCounter++}`;
32
+ let finalDefinition = definition;
33
+
34
+ // transform theme to mermaid config
35
+ const mermaidConfig: Record<string, string> = {};
36
+ if (options.theme) {
37
+ mermaidConfig.theme = options.theme;
38
+ } else {
39
+ mermaidConfig.theme = defaultTheme;
40
+ }
41
+
42
+ // If we have config to apply, prepend the directive
43
+ if (Object.keys(mermaidConfig).length > 0) {
44
+ const jsonConfig = JSON.stringify(mermaidConfig);
45
+ // Mermaid directive format: %%{init: { ... }}%%
46
+ finalDefinition = `%%{init: ${jsonConfig} }%%\n${definition}`;
47
+ }
48
+
49
+ const { svg } = await mermaid.render(id, finalDefinition);
50
+ return { svg, error: null };
51
+ } catch (e) {
52
+ const errorMsg = e instanceof Error ? e.message : "Unknown error";
53
+ return { svg: "", error: errorMsg };
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Helper to parse attributes from fence line
59
+ * Example: ```mermaid theme="dark" scale="2"
60
+ */
61
+ function parseAttributes(fenceLine: string): Record<string, string> {
62
+ const attributes: Record<string, string> = {};
63
+ // Match key="value" or key='value'
64
+ const regex = /(\w+)=["']([^"']*)["']/g;
65
+ let match;
66
+ while ((match = regex.exec(fenceLine)) !== null && match[1] && match[2]) {
67
+ attributes[match[1]] = match[2];
68
+ }
69
+ return attributes;
70
+ }
71
+
72
+ /**
73
+ * Mark decorations for mermaid syntax elements
74
+ */
75
+ const mermaidMarkDecorations = {
76
+ "mermaid-block-start": Decoration.line({ class: "cm-mardora-mermaid-block-start" }),
77
+ "mermaid-block-end": Decoration.line({ class: "cm-mardora-mermaid-block-end" }),
78
+ "mermaid-block": Decoration.line({ class: "cm-mardora-mermaid-block" }),
79
+ "mermaid-block-rendered": Decoration.line({ class: "cm-mardora-mermaid-block-rendered" }),
80
+ "mermaid-marker": Decoration.mark({ class: "cm-mardora-mermaid-marker" }),
81
+ "mermaid-hidden": Decoration.mark({ class: "cm-mardora-mermaid-hidden" }),
82
+ };
83
+
84
+ /**
85
+ * Widget to render mermaid block diagrams
86
+ */
87
+ class MermaidBlockWidget extends WidgetType {
88
+ constructor(
89
+ readonly definition: string,
90
+ readonly attributes: Record<string, string>,
91
+ readonly defaultTheme: string,
92
+ readonly from: number,
93
+ readonly to: number
94
+ ) {
95
+ super();
96
+ }
97
+
98
+ override eq(other: MermaidBlockWidget): boolean {
99
+ return (
100
+ other.definition === this.definition &&
101
+ JSON.stringify(other.attributes) === JSON.stringify(this.attributes) &&
102
+ other.defaultTheme === this.defaultTheme &&
103
+ other.from === this.from &&
104
+ other.to === this.to
105
+ );
106
+ }
107
+
108
+ toDOM(view: EditorView) {
109
+ const div = document.createElement("div");
110
+ div.className = "cm-mardora-mermaid-rendered cm-mardora-media-preview";
111
+ div.style.cursor = "pointer";
112
+
113
+ // Show loading state initially
114
+ div.innerHTML = `<div class="cm-mardora-mermaid-loading">Rendering diagram…</div>`;
115
+
116
+ // Render mermaid asynchronously
117
+ // Render mermaid asynchronously
118
+ renderMermaid(this.definition, this.attributes, this.defaultTheme).then(({ svg, error }) => {
119
+ if (error) {
120
+ div.className += " cm-mardora-mermaid-error";
121
+ div.innerHTML = `<span>[Mermaid Error: ${error}]</span>`;
122
+ } else {
123
+ div.innerHTML = svg;
124
+ div.appendChild(
125
+ createMediaPreviewButton(div.ownerDocument, {
126
+ label: "放大查看 Mermaid 图",
127
+ content: () => ({ kind: "html", html: svg, title: "Mermaid 图" }),
128
+ })
129
+ );
130
+ }
131
+ });
132
+
133
+ // Click handler to select the raw mermaid text
134
+ div.addEventListener("click", (e) => {
135
+ e.preventDefault();
136
+ e.stopPropagation();
137
+ view.dispatch({
138
+ selection: { anchor: this.from, head: this.to },
139
+ scrollIntoView: true,
140
+ });
141
+ view.focus();
142
+ });
143
+
144
+ return div;
145
+ }
146
+
147
+ override ignoreEvent(event: Event) {
148
+ return event.type !== "click";
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Block parser for mermaid blocks:
154
+ * ```mermaid
155
+ * graph TD
156
+ * A --> B
157
+ * ```
158
+ */
159
+ const mermaidBlockParser: BlockParser = {
160
+ name: "MermaidBlock",
161
+ before: "FencedCode",
162
+ parse(cx: BlockContext, line: Line) {
163
+ const text = line.text;
164
+ const trimmed = text.slice(line.pos).trimStart();
165
+
166
+ // Must start with ```mermaid
167
+ if (!trimmed.startsWith("```mermaid")) return false;
168
+
169
+ // Ensure nothing meaningful after ```mermaid (allow trailing whitespace)
170
+ // We now allow attributes, so we don't strictly check for empty rest
171
+ // const rest = trimmed.slice(10);
172
+ // if (rest.trim().length > 0) return false;
173
+
174
+ const startLine = cx.lineStart;
175
+ let endPos = -1;
176
+ let closeBacktickStart = -1;
177
+
178
+ // Move past the opening line and find the closing ```
179
+ while (cx.nextLine()) {
180
+ const currentText = line.text;
181
+ const currentLineStart = cx.lineStart;
182
+ const lastLineEnd = currentLineStart + currentText.length;
183
+
184
+ // Check if this line is a closing ``` (only backticks, possibly with whitespace)
185
+ const trimmedLine = currentText.trim();
186
+ if (trimmedLine === "```") {
187
+ endPos = lastLineEnd;
188
+ closeBacktickStart = currentLineStart + currentText.indexOf("```");
189
+ // Move past the closing line so subsequent markdown gets parsed
190
+ cx.nextLine();
191
+ break;
192
+ }
193
+ }
194
+
195
+ if (endPos === -1) {
196
+ // No closing found, treat as regular text
197
+ return false;
198
+ }
199
+
200
+ // Create the mermaid block element with markers
201
+ const openMarkEnd = startLine + text.indexOf("```mermaid") + 10;
202
+ const openMark = cx.elt("MermaidBlockMark", startLine, openMarkEnd);
203
+ const closeMark = cx.elt("MermaidBlockMark", closeBacktickStart, closeBacktickStart + 3);
204
+
205
+ cx.addElement(cx.elt("MermaidBlock", startLine, endPos, [openMark, closeMark]));
206
+
207
+ return true;
208
+ },
209
+ };
210
+
211
+ /**
212
+ * MermaidPlugin - Renders mermaid diagrams in the editor
213
+ *
214
+ * Supports block mermaid syntax:
215
+ * ```mermaid
216
+ * graph TD
217
+ * A --> B
218
+ * ```
219
+ *
220
+ * Behavior:
221
+ * - Always show rendered diagram below the block
222
+ * - Hide raw definition when cursor is outside the block
223
+ * - Show raw definition with styled markers when cursor is inside
224
+ */
225
+ export class MermaidPlugin extends DecorationPlugin {
226
+ readonly name = "mermaid";
227
+ readonly version = "1.0.0";
228
+ override decorationPriority = 25;
229
+ override readonly requiredNodes = ["MermaidBlock", "MermaidBlockMark"] as const;
230
+
231
+ constructor() {
232
+ super();
233
+ }
234
+
235
+ /**
236
+ * Plugin theme
237
+ */
238
+ override get theme() {
239
+ return theme;
240
+ }
241
+
242
+ /**
243
+ * Return markdown parser extensions for mermaid syntax
244
+ */
245
+ override getMarkdownConfig(): MarkdownConfig {
246
+ return {
247
+ defineNodes: [
248
+ { name: "MermaidBlock", block: true },
249
+ { name: "MermaidBlockMark", style: tags.processingInstruction },
250
+ ],
251
+ parseBlock: [mermaidBlockParser],
252
+ };
253
+ }
254
+
255
+ /**
256
+ * Build decorations for mermaid blocks
257
+ */
258
+ buildDecorations(ctx: DecorationContext): void {
259
+ const { view, decorations } = ctx;
260
+ const tree = syntaxTree(view.state);
261
+ const config = this.context?.config;
262
+ const currentTheme = config?.theme === ThemeEnum.DARK ? "dark" : "default";
263
+
264
+ tree.iterate({
265
+ enter: (node) => {
266
+ const { from, to, name } = node;
267
+
268
+ if (name === "MermaidBlock") {
269
+ const content = view.state.sliceDoc(from, to);
270
+
271
+ // Extract mermaid definition (remove ```mermaid and ``` markers)
272
+ const lines = content.split("\n");
273
+ const definition = lines
274
+ .slice(1, -1) // Remove first and last lines (the markers)
275
+ .join("\n")
276
+ .trim();
277
+
278
+ const docLines = content.split("\n");
279
+ const fenceLine = docLines[0] || "";
280
+ const attributes = parseAttributes(fenceLine);
281
+
282
+ const nodeLineStart = view.state.doc.lineAt(from);
283
+ const nodeLineEnd = view.state.doc.lineAt(to);
284
+ const cursorInRange = ctx.selectionOverlapsRange(nodeLineStart.from, nodeLineEnd.to);
285
+
286
+ // Calculate line number width for mermaid block
287
+ const totalCodeLines = nodeLineEnd.number - nodeLineStart.number - 1;
288
+ const lineNumWidth = String(totalCodeLines).length;
289
+ let codeLineIndex = 1;
290
+
291
+ // Add line decorations for mermaid block
292
+ for (let i = nodeLineStart.number; i <= nodeLineEnd.number; i++) {
293
+ const line = view.state.doc.line(i);
294
+ const isFenceLine = i === nodeLineStart.number || i === nodeLineEnd.number;
295
+ const relativeLineNum = codeLineIndex;
296
+
297
+ decorations.push(mermaidMarkDecorations["mermaid-block"].range(line.from));
298
+ if (!cursorInRange) decorations.push(mermaidMarkDecorations["mermaid-block-rendered"].range(line.from));
299
+
300
+ if (i === nodeLineStart.number)
301
+ decorations.push(mermaidMarkDecorations["mermaid-block-start"].range(line.from));
302
+
303
+ if (i === nodeLineEnd.number)
304
+ decorations.push(mermaidMarkDecorations["mermaid-block-end"].range(line.from));
305
+
306
+ if (!isFenceLine) {
307
+ decorations.push(
308
+ Decoration.line({
309
+ attributes: {
310
+ "data-line-num": String(relativeLineNum),
311
+ style: `--line-num-width: ${lineNumWidth}ch`,
312
+ },
313
+ }).range(line.from)
314
+ );
315
+ }
316
+
317
+ // Increment code line index (only for non-fence lines)
318
+ if (!isFenceLine) {
319
+ codeLineIndex++;
320
+ }
321
+ }
322
+
323
+ // Always add the rendered widget below the block
324
+ decorations.push(
325
+ Decoration.widget({
326
+ widget: new MermaidBlockWidget(definition, attributes, currentTheme, from, to),
327
+ side: 1,
328
+ block: false,
329
+ }).range(to)
330
+ );
331
+
332
+ if (cursorInRange) {
333
+ // Cursor in range: show raw definition with styled markers
334
+ for (let child = node.node.firstChild; child; child = child.nextSibling) {
335
+ if (child.name === "MermaidBlockMark") {
336
+ decorations.push(mermaidMarkDecorations["mermaid-marker"].range(child.from, child.to));
337
+ }
338
+ }
339
+ } else {
340
+ // Cursor out of range: hide the raw text
341
+ decorations.push(mermaidMarkDecorations["mermaid-hidden"].range(from, to));
342
+ }
343
+ }
344
+ },
345
+ });
346
+ }
347
+
348
+ /**
349
+ * Render mermaid to HTML for preview mode
350
+ *
351
+ * Renders the actual mermaid diagram to SVG HTML
352
+ */
353
+ override async renderToHTML(
354
+ node: SyntaxNode,
355
+ _children: string,
356
+ ctx: { sliceDoc(from: number, to: number): string; sanitize(html: string): string }
357
+ ): Promise<string | null> {
358
+ if (node.name === "MermaidBlock") {
359
+ const content = ctx.sliceDoc(node.from, node.to);
360
+ const lines = content.split("\n");
361
+ const definition = lines.length > 1 ? lines.slice(1, -1).join("\n").trim() : "";
362
+
363
+ const fenceLine = lines[0] || "";
364
+ const attributes = parseAttributes(fenceLine);
365
+
366
+ const config = this.context?.config;
367
+ const currentTheme = config?.theme === ThemeEnum.DARK ? "dark" : "default";
368
+
369
+ const { svg, error } = await renderMermaid(definition, attributes, currentTheme);
370
+
371
+ if (error) {
372
+ return `<div class="cm-mardora-mermaid-error">${ctx.sanitize(`[Mermaid Error: ${error}]`)}</div>`;
373
+ }
374
+
375
+ return `<div class="cm-mardora-mermaid-rendered">${svg}</div>`;
376
+ }
377
+
378
+ // Hide mermaid markers in preview
379
+ if (node.name === "MermaidBlockMark") {
380
+ return "";
381
+ }
382
+
383
+ return null;
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Theme for mermaid styling
389
+ */
390
+ const mermaidTheme = createTheme({
391
+ default: {
392
+ // Raw mermaid block lines (monospace)
393
+ ".cm-mardora-mermaid-block:not(.cm-mardora-mermaid-block-rendered)": {
394
+ "--radius": "0.375rem",
395
+ position: "relative",
396
+
397
+ fontFamily: "var(--font-jetbrains-mono, monospace)",
398
+ fontSize: "0.9rem",
399
+ backgroundColor: "rgba(0, 0, 0, 0.03)",
400
+ padding: "0 1rem !important",
401
+ paddingLeft: "calc(var(--line-num-width, 2ch) + 1rem) !important",
402
+ lineHeight: "1.5",
403
+ borderLeft: "1px solid var(--color-border)",
404
+ borderRight: "1px solid var(--color-border)",
405
+ },
406
+
407
+ ".cm-mardora-mermaid-block-start:not(.cm-mardora-mermaid-block-rendered)": {
408
+ overflow: "hidden",
409
+ borderTopLeftRadius: "var(--radius)",
410
+ borderTopRightRadius: "var(--radius)",
411
+ borderTop: "1px solid var(--color-border)",
412
+ },
413
+
414
+ ".cm-mardora-mermaid-block-end:not(.cm-mardora-mermaid-block-rendered)": {
415
+ overflow: "hidden",
416
+ borderBottomLeftRadius: "var(--radius)",
417
+ borderBottomRightRadius: "var(--radius)",
418
+ borderBottom: "1px solid var(--color-border)",
419
+ },
420
+
421
+ ".cm-mardora-mermaid-block:not(.cm-mardora-mermaid-block-rendered)::before": {
422
+ content: "attr(data-line-num)",
423
+ position: "absolute",
424
+ left: "0.5rem",
425
+ top: "0.2rem",
426
+ width: "var(--line-num-width, 2ch)",
427
+ textAlign: "right",
428
+ color: "#6a737d",
429
+ opacity: "0.6",
430
+ fontFamily: "var(--font-jetbrains-mono, monospace)",
431
+ fontSize: "0.85rem",
432
+ userSelect: "none",
433
+ },
434
+
435
+ ".cm-mardora-mermaid-block.cm-mardora-mermaid-block-rendered br": {
436
+ display: "none",
437
+ },
438
+
439
+ // Mermaid markers (```mermaid / ```)
440
+ ".cm-mardora-mermaid-marker": {
441
+ color: "#6a737d",
442
+ fontFamily: "var(--font-jetbrains-mono, monospace)",
443
+ },
444
+
445
+ // Hidden mermaid syntax (when cursor is not in range)
446
+ ".cm-mardora-mermaid-hidden": {
447
+ display: "none",
448
+ },
449
+
450
+ // Rendered mermaid container
451
+ ".cm-mardora-mermaid-rendered": {
452
+ display: "flex",
453
+ justifyContent: "center",
454
+ alignItems: "center",
455
+ padding: "1em 0",
456
+ borderRadius: "4px",
457
+ overflow: "auto",
458
+ },
459
+
460
+ // SVG inside rendered container
461
+ ".cm-mardora-mermaid-rendered svg": {
462
+ maxWidth: "100%",
463
+ height: "auto",
464
+ aspectRatio: "auto",
465
+ },
466
+
467
+ // Loading state
468
+ ".cm-mardora-mermaid-loading": {
469
+ display: "inline-block",
470
+ padding: "0.5em 1em",
471
+ color: "#6a737d",
472
+ fontSize: "0.875em",
473
+ fontStyle: "italic",
474
+ fontFamily: "var(--font-jetbrains-mono, monospace)",
475
+ },
476
+
477
+ // Error styling
478
+ ".cm-mardora-mermaid-error": {
479
+ display: "inline-block",
480
+ padding: "0.25em 0.5em",
481
+ backgroundColor: "rgba(255, 0, 0, 0.1)",
482
+ color: "#d73a49",
483
+ borderRadius: "4px",
484
+ fontSize: "0.875em",
485
+ fontStyle: "italic",
486
+ fontFamily: "var(--font-jetbrains-mono, monospace)",
487
+ },
488
+ },
489
+
490
+ dark: {
491
+ ".cm-mardora-mermaid-block:not(.cm-mardora-mermaid-block-rendered)": {
492
+ backgroundColor: "rgba(255, 255, 255, 0.03)",
493
+ },
494
+
495
+ ".cm-mardora-mermaid-marker": {
496
+ color: "#8b949e",
497
+ },
498
+
499
+ ".cm-mardora-mermaid-loading": {
500
+ color: "#8b949e",
501
+ },
502
+
503
+ ".cm-mardora-mermaid-error": {
504
+ backgroundColor: "rgba(255, 0, 0, 0.15)",
505
+ color: "#f85149",
506
+ },
507
+ },
508
+ });
509
+
510
+ const theme = (theme: Parameters<typeof mermaidTheme>[0]) => ({
511
+ ...mermaidTheme(theme),
512
+ ...mediaLightboxTheme(theme),
513
+ });
@@ -0,0 +1,38 @@
1
+ import { SyntaxNode } from "@lezer/common";
2
+ import { MardoraPlugin } from "../editor/plugin";
3
+ import { createTheme } from "../editor";
4
+
5
+ /**
6
+ * ParagraphPlugin - Adds top and bottom padding to paragraphs in preview
7
+ *
8
+ * Applies visual spacing to markdown paragraphs for better readability
9
+ */
10
+ export class ParagraphPlugin extends MardoraPlugin {
11
+ readonly name = "paragraph";
12
+ readonly version = "1.0.0";
13
+ override readonly requiredNodes = ["Paragraph"] as const;
14
+
15
+ /**
16
+ * Plugin theme for preview styling
17
+ */
18
+ override get theme() {
19
+ return theme;
20
+ }
21
+
22
+ override renderToHTML(node: SyntaxNode, children: string): string | null {
23
+ if (node.name !== "Paragraph") {
24
+ return null;
25
+ }
26
+
27
+ return `<p class="cm-mardora-paragraph">${children}</p>`;
28
+ }
29
+ }
30
+
31
+ const theme = createTheme({
32
+ default: {
33
+ ".cm-mardora-paragraph": {
34
+ paddingTop: "0.5em",
35
+ paddingBottom: "0.5em",
36
+ },
37
+ },
38
+ });