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,146 @@
1
+ import { createTheme } from "./utils";
2
+
3
+ export const mediaLightboxTheme = createTheme({
4
+ default: {
5
+ ".cm-mardora-media-preview": {
6
+ position: "relative",
7
+ },
8
+ ".cm-mardora-media-preview-button": {
9
+ position: "absolute",
10
+ top: "0.5rem",
11
+ right: "0.5rem",
12
+ width: "1.75rem",
13
+ height: "1.75rem",
14
+ border: "1px solid rgba(148, 163, 184, 0.45)",
15
+ borderRadius: "0.375rem",
16
+ backgroundColor: "rgba(255, 255, 255, 0.86)",
17
+ color: "#334155",
18
+ display: "inline-flex",
19
+ alignItems: "center",
20
+ justifyContent: "center",
21
+ padding: "0",
22
+ opacity: "0",
23
+ pointerEvents: "auto",
24
+ cursor: "pointer",
25
+ transition: "opacity 120ms ease, background-color 120ms ease, border-color 120ms ease",
26
+ zIndex: "2",
27
+ },
28
+ ".cm-mardora-media-preview:hover .cm-mardora-media-preview-button, .cm-mardora-media-preview-button:focus-visible": {
29
+ opacity: "1",
30
+ },
31
+ ".cm-mardora-media-preview-button:hover, .cm-mardora-media-preview-button:focus-visible": {
32
+ borderColor: "rgba(37, 99, 235, 0.55)",
33
+ backgroundColor: "rgba(255, 255, 255, 0.96)",
34
+ color: "#2563eb",
35
+ outline: "none",
36
+ },
37
+ ".cm-mardora-media-preview-button svg": {
38
+ width: "1rem",
39
+ height: "1rem",
40
+ },
41
+ ".cm-mardora-media-lightbox": {
42
+ position: "fixed",
43
+ inset: "0",
44
+ zIndex: "10000",
45
+ display: "flex",
46
+ alignItems: "center",
47
+ justifyContent: "center",
48
+ padding: "2rem",
49
+ backgroundColor: "rgba(15, 23, 42, 0.24)",
50
+ },
51
+ ".cm-mardora-media-lightbox-panel": {
52
+ position: "relative",
53
+ width: "min(96vw, 1200px)",
54
+ maxWidth: "min(96vw, 1200px)",
55
+ maxHeight: "92vh",
56
+ borderRadius: "0.75rem",
57
+ backgroundColor: "#ffffff",
58
+ color: "#0f172a",
59
+ boxShadow: "0 24px 70px rgba(15, 23, 42, 0.28)",
60
+ overflow: "hidden",
61
+ },
62
+ ".cm-mardora-media-lightbox-body": {
63
+ maxWidth: "100%",
64
+ maxHeight: "92vh",
65
+ overflow: "auto",
66
+ display: "flex",
67
+ alignItems: "center",
68
+ justifyContent: "center",
69
+ padding: "1rem",
70
+ },
71
+ ".cm-mardora-media-lightbox-image": {
72
+ display: "block",
73
+ maxWidth: "100%",
74
+ maxHeight: "calc(92vh - 2rem)",
75
+ width: "auto",
76
+ height: "auto",
77
+ objectFit: "contain",
78
+ },
79
+ ".cm-mardora-media-lightbox-html": {
80
+ width: "100%",
81
+ maxWidth: "100%",
82
+ maxHeight: "calc(92vh - 2rem)",
83
+ overflow: "auto",
84
+ display: "flex",
85
+ justifyContent: "center",
86
+ },
87
+ ".cm-mardora-media-lightbox-html svg": {
88
+ display: "block",
89
+ maxWidth: "100%",
90
+ maxHeight: "calc(92vh - 2rem)",
91
+ width: "100%",
92
+ height: "auto",
93
+ },
94
+ ".cm-mardora-media-lightbox-close": {
95
+ position: "absolute",
96
+ top: "0.5rem",
97
+ right: "0.5rem",
98
+ width: "2rem",
99
+ height: "2rem",
100
+ border: "1px solid rgba(148, 163, 184, 0.45)",
101
+ borderRadius: "0.375rem",
102
+ backgroundColor: "rgba(255, 255, 255, 0.92)",
103
+ color: "#334155",
104
+ display: "inline-flex",
105
+ alignItems: "center",
106
+ justifyContent: "center",
107
+ padding: "0",
108
+ cursor: "pointer",
109
+ },
110
+ ".cm-mardora-media-lightbox-close:hover, .cm-mardora-media-lightbox-close:focus-visible": {
111
+ borderColor: "rgba(37, 99, 235, 0.55)",
112
+ color: "#2563eb",
113
+ outline: "none",
114
+ },
115
+ ".cm-mardora-media-lightbox-close svg": {
116
+ width: "1rem",
117
+ height: "1rem",
118
+ },
119
+ },
120
+ dark: {
121
+ ".cm-mardora-media-preview-button": {
122
+ borderColor: "rgba(71, 85, 105, 0.72)",
123
+ backgroundColor: "rgba(15, 23, 42, 0.84)",
124
+ color: "#cbd5e1",
125
+ },
126
+ ".cm-mardora-media-preview-button:hover, .cm-mardora-media-preview-button:focus-visible": {
127
+ borderColor: "rgba(96, 165, 250, 0.68)",
128
+ backgroundColor: "rgba(15, 23, 42, 0.96)",
129
+ color: "#93c5fd",
130
+ },
131
+ ".cm-mardora-media-lightbox-panel": {
132
+ backgroundColor: "#0f172a",
133
+ color: "#e2e8f0",
134
+ boxShadow: "0 24px 70px rgba(0, 0, 0, 0.45)",
135
+ },
136
+ ".cm-mardora-media-lightbox-close": {
137
+ borderColor: "rgba(71, 85, 105, 0.72)",
138
+ backgroundColor: "rgba(15, 23, 42, 0.92)",
139
+ color: "#cbd5e1",
140
+ },
141
+ ".cm-mardora-media-lightbox-close:hover, .cm-mardora-media-lightbox-close:focus-visible": {
142
+ borderColor: "rgba(96, 165, 250, 0.68)",
143
+ color: "#93c5fd",
144
+ },
145
+ },
146
+ });
@@ -0,0 +1,125 @@
1
+ import { createMardoraIcon } from "./icons";
2
+
3
+ export type MediaLightboxContent =
4
+ | {
5
+ readonly kind: "image";
6
+ readonly src: string;
7
+ readonly alt: string;
8
+ readonly title?: string;
9
+ }
10
+ | {
11
+ readonly kind: "html";
12
+ readonly html: string;
13
+ readonly title?: string;
14
+ };
15
+
16
+ export interface OpenMediaLightboxOptions {
17
+ readonly content: MediaLightboxContent;
18
+ readonly returnFocus?: HTMLElement | null;
19
+ }
20
+
21
+ export interface MediaLightboxHandle {
22
+ readonly element: HTMLElement;
23
+ close(): void;
24
+ }
25
+
26
+ export interface MediaPreviewButtonOptions {
27
+ readonly label: string;
28
+ readonly content: () => MediaLightboxContent | null;
29
+ }
30
+
31
+ export function consumeMediaLightboxTrigger(event: Pick<Event, "preventDefault" | "stopPropagation">): void {
32
+ event.preventDefault();
33
+ event.stopPropagation();
34
+ }
35
+
36
+ export function createMediaPreviewButton(
37
+ ownerDocument: Document,
38
+ options: MediaPreviewButtonOptions
39
+ ): HTMLButtonElement {
40
+ const button = ownerDocument.createElement("button");
41
+ button.type = "button";
42
+ button.className = "cm-mardora-media-preview-button";
43
+ button.setAttribute("aria-label", options.label);
44
+ button.title = options.label;
45
+ const icon = createMardoraIcon("maximize-2");
46
+ if (icon) button.appendChild(icon);
47
+ button.addEventListener("click", (event) => {
48
+ consumeMediaLightboxTrigger(event);
49
+ const content = options.content();
50
+ if (!content) return;
51
+ openMediaLightbox(ownerDocument, { content, returnFocus: button });
52
+ });
53
+ return button;
54
+ }
55
+
56
+ export function openMediaLightbox(ownerDocument: Document, options: OpenMediaLightboxOptions): MediaLightboxHandle {
57
+ ownerDocument.querySelector(".cm-mardora-media-lightbox")?.remove();
58
+ const mountPoint = options.returnFocus?.closest(".cm-editor") ?? ownerDocument.body;
59
+
60
+ const root = ownerDocument.createElement("div");
61
+ root.className = "cm-mardora-media-lightbox";
62
+ root.setAttribute("role", "dialog");
63
+ root.setAttribute("aria-modal", "true");
64
+ if (options.content.title) {
65
+ root.setAttribute("aria-label", options.content.title);
66
+ }
67
+
68
+ const panel = ownerDocument.createElement("div");
69
+ panel.className = "cm-mardora-media-lightbox-panel";
70
+ panel.addEventListener("click", (event) => event.stopPropagation());
71
+
72
+ const closeButton = ownerDocument.createElement("button");
73
+ closeButton.type = "button";
74
+ closeButton.className = "cm-mardora-media-lightbox-close";
75
+ closeButton.setAttribute("aria-label", "关闭大图查看");
76
+ closeButton.title = "关闭大图查看";
77
+ const closeIcon = createMardoraIcon("x");
78
+ if (closeIcon) closeButton.appendChild(closeIcon);
79
+
80
+ const body = ownerDocument.createElement("div");
81
+ body.className = "cm-mardora-media-lightbox-body";
82
+ body.appendChild(createLightboxContent(ownerDocument, options.content));
83
+
84
+ panel.appendChild(closeButton);
85
+ panel.appendChild(body);
86
+ root.appendChild(panel);
87
+
88
+ const close = (): void => {
89
+ root.remove();
90
+ ownerDocument.removeEventListener("keydown", handleKeydown);
91
+ options.returnFocus?.focus();
92
+ };
93
+ const handleKeydown = (event: KeyboardEvent): void => {
94
+ if (event.key === "Escape") {
95
+ close();
96
+ }
97
+ };
98
+
99
+ root.addEventListener("click", close);
100
+ closeButton.addEventListener("click", (event) => {
101
+ consumeMediaLightboxTrigger(event);
102
+ close();
103
+ });
104
+ ownerDocument.addEventListener("keydown", handleKeydown);
105
+ mountPoint.appendChild(root);
106
+ closeButton.focus();
107
+
108
+ return { element: root, close };
109
+ }
110
+
111
+ function createLightboxContent(ownerDocument: Document, content: MediaLightboxContent): HTMLElement {
112
+ if (content.kind === "image") {
113
+ const image = ownerDocument.createElement("img");
114
+ image.className = "cm-mardora-media-lightbox-image";
115
+ image.src = content.src;
116
+ image.alt = content.alt;
117
+ if (content.title) image.title = content.title;
118
+ return image;
119
+ }
120
+
121
+ const container = ownerDocument.createElement("div");
122
+ container.className = "cm-mardora-media-lightbox-html";
123
+ container.innerHTML = content.html;
124
+ return container;
125
+ }
@@ -0,0 +1,294 @@
1
+ import { Decoration, EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
2
+ import { Extension, Range } from "@codemirror/state";
3
+ import { MarkdownConfig } from "@lezer/markdown";
4
+ import { SyntaxNode } from "@lezer/common";
5
+ import { MardoraConfig } from "./mardora";
6
+ import { createTheme, ThemeEnum, ThemeStyle } from "./utils";
7
+ import { StyleModule } from "style-mod";
8
+
9
+ /**
10
+ * Context passed to plugin lifecycle methods
11
+ */
12
+ export interface PluginContext {
13
+ /** Current configuration */
14
+ readonly config: MardoraConfig;
15
+ }
16
+
17
+ /**
18
+ * Plugin configuration schema
19
+ */
20
+ export interface PluginConfig {
21
+ [key: string]: unknown;
22
+ }
23
+
24
+ /**
25
+ * Decoration context passed to plugin decoration builders
26
+ * Provides access to view state and decoration collection
27
+ */
28
+ export interface DecorationContext {
29
+ /** The EditorView instance (readonly) */
30
+ readonly view: EditorView;
31
+
32
+ /** Array to push decorations into (will be sorted automatically) */
33
+ readonly decorations: Range<Decoration>[];
34
+
35
+ /** Check if selection overlaps with a range (to show raw markdown) */
36
+ selectionOverlapsRange(from: number, to: number): boolean;
37
+
38
+ /** Check if cursor is within a range */
39
+ cursorInRange(from: number, to: number): boolean;
40
+ }
41
+
42
+ /**
43
+ * Abstract base class for all mardora plugins
44
+ *
45
+ * Implements OOP principles:
46
+ * - Abstraction: abstract name/version must be implemented by subclasses
47
+ * - Encapsulation: private _config, protected _context
48
+ * - Inheritance: specialized plugin classes can extend this
49
+ */
50
+ export abstract class MardoraPlugin {
51
+ /** Unique plugin identifier (abstract - must be implemented) */
52
+ abstract readonly name: string;
53
+
54
+ /** Plugin version (abstract - must be implemented) */
55
+ abstract readonly version: string;
56
+
57
+ /** Decoration priority (higher = applied later) */
58
+ readonly decorationPriority: number = 100;
59
+
60
+ /** Plugin dependencies - names of required plugins */
61
+ readonly dependencies: string[] = [];
62
+
63
+ /** Node types this plugin handles for decorations and preview rendering */
64
+ readonly requiredNodes: readonly string[] = [];
65
+
66
+ /** Private configuration storage */
67
+ private _config: PluginConfig = {};
68
+
69
+ /** Protected context - accessible to subclasses */
70
+ protected _context: PluginContext | null = null;
71
+
72
+ /** Get plugin configuration */
73
+ get config(): PluginConfig {
74
+ return this._config;
75
+ }
76
+
77
+ /** Set plugin configuration */
78
+ set config(value: PluginConfig) {
79
+ this._config = value;
80
+ }
81
+
82
+ /** Get plugin context */
83
+ get context(): PluginContext | null {
84
+ return this._context;
85
+ }
86
+
87
+ /** Plugin theme */
88
+ get theme(): (theme: ThemeEnum) => ThemeStyle {
89
+ return createTheme({
90
+ default: {},
91
+ dark: {},
92
+ light: {},
93
+ });
94
+ }
95
+
96
+ // ============================================
97
+ // EXTENSION METHODS (overridable by subclasses)
98
+ // ============================================
99
+
100
+ /**
101
+ * Return CodeMirror extensions for this plugin
102
+ * Override to provide custom extensions
103
+ */
104
+ getExtensions(): Extension[] {
105
+ return [];
106
+ }
107
+
108
+ /**
109
+ * Return markdown parser extensions
110
+ * Override to extend markdown parsing
111
+ */
112
+ getMarkdownConfig(): MarkdownConfig | null {
113
+ return null;
114
+ }
115
+
116
+ /**
117
+ * Return keybindings for this plugin
118
+ * Override to add custom keyboard shortcuts
119
+ */
120
+ getKeymap(): KeyBinding[] {
121
+ return [];
122
+ }
123
+
124
+ // ============================================
125
+ // DECORATION METHODS (overridable by subclasses)
126
+ // ============================================
127
+
128
+ /**
129
+ * Build decorations for the current view state
130
+ * Override to contribute decorations to the editor
131
+ *
132
+ * @param ctx - Decoration context with view and decoration array
133
+ */
134
+ buildDecorations(_ctx: DecorationContext): void {
135
+ void _ctx;
136
+ // Default implementation does nothing
137
+ // Subclasses override to add decorations
138
+ }
139
+
140
+ // ============================================
141
+ // LIFECYCLE HOOKS (overridable by subclasses)
142
+ // ============================================
143
+
144
+ /**
145
+ * Called when plugin is registered with mardora
146
+ * Override to perform initialization
147
+ *
148
+ * @param context - Plugin context with configuration
149
+ */
150
+ onRegister(context: PluginContext): void | Promise<void> {
151
+ this._context = context;
152
+ }
153
+
154
+ /**
155
+ * Called when plugin is unregistered
156
+ * Override to perform cleanup
157
+ */
158
+ onUnregister(): void {
159
+ this._context = null;
160
+ }
161
+
162
+ /**
163
+ * Called when EditorView is created and ready
164
+ * Override to perform view-specific initialization
165
+ *
166
+ * @param view - The EditorView instance
167
+ */
168
+ onViewReady(_view: EditorView): void {
169
+ void _view;
170
+ // Default implementation does nothing
171
+ }
172
+
173
+ /**
174
+ * Called on view updates (document changes, selection changes, etc.)
175
+ * Override to react to editor changes
176
+ *
177
+ * @param update - The ViewUpdate with change information
178
+ */
179
+ onViewUpdate(_update: ViewUpdate): void {
180
+ void _update;
181
+ // Default implementation does nothing
182
+ }
183
+
184
+ // ============================================
185
+ // PROTECTED UTILITIES (for subclasses)
186
+ // ============================================
187
+
188
+ /**
189
+ * Helper to get current editor state
190
+ * @param view - The EditorView instance
191
+ */
192
+ protected getState(view: EditorView) {
193
+ return view.state;
194
+ }
195
+
196
+ /**
197
+ * Helper to get current document
198
+ * @param view - The EditorView instance
199
+ */
200
+ protected getDocument(view: EditorView) {
201
+ return view.state.doc;
202
+ }
203
+
204
+ // ============================================
205
+ // PREVIEW RENDERING METHODS (for mardora/preview)
206
+ // ============================================
207
+
208
+ /**
209
+ * Render a syntax node to HTML for preview mode
210
+ * Override to provide custom HTML rendering for specific node types
211
+ *
212
+ * @param node - The syntax node to render
213
+ * @param children - Pre-rendered children HTML
214
+ * @param ctx - Preview context with document and utilities
215
+ * @returns HTML string to use, or null to use default rendering
216
+ */
217
+ renderToHTML?(
218
+ node: SyntaxNode,
219
+ children: string,
220
+ ctx: {
221
+ doc: string;
222
+ sliceDoc(from: number, to: number): string;
223
+ sanitize(html: string): string;
224
+ syntaxHighlighters?: readonly import("@lezer/highlight").Highlighter[];
225
+ }
226
+ ): string | null | Promise<string | null>;
227
+
228
+ /**
229
+ * Return the document position consumed by preview rendering.
230
+ * Plugins can use this when a rendered node owns trailing syntax that is
231
+ * outside the Lezer node range, such as image attributes.
232
+ */
233
+ getPreviewConsumedTo?(
234
+ node: SyntaxNode,
235
+ ctx: {
236
+ doc: string;
237
+ sliceDoc(from: number, to: number): string;
238
+ }
239
+ ): number | null;
240
+
241
+ /**
242
+ * Get CSS styles for preview mode
243
+ * Override to provide custom CSS for preview rendering
244
+ *
245
+ * @param theme - Current theme enum
246
+ * @returns CSS string for preview styles
247
+ */
248
+ getPreviewStyles(theme: ThemeEnum, wrapperClass: string): string {
249
+ const themeStyles = this.theme(theme);
250
+ return this.transformToCss(themeStyles, wrapperClass);
251
+ }
252
+
253
+ /**
254
+ * Transform ThemeStyle object to CSS string for preview
255
+ * Uses cssClassMap to convert CM selectors to semantic selectors
256
+ */
257
+ protected transformToCss(themeStyles: ThemeStyle, wrapperClass: string): string {
258
+ const styleMod = new StyleModule(themeStyles, {
259
+ finish: (sel) => {
260
+ return `.${wrapperClass} ${sel}`;
261
+ },
262
+ });
263
+ return styleMod.getRules();
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Base class for plugins that primarily contribute decorations
269
+ * Extends MardoraPlugin with decoration-focused defaults
270
+ */
271
+ export abstract class DecorationPlugin extends MardoraPlugin {
272
+ /**
273
+ * Decoration priority - lower than default for decoration plugins
274
+ * Override to customize
275
+ */
276
+ override decorationPriority = 50;
277
+
278
+ /**
279
+ * Subclasses must implement this to provide decorations
280
+ * @param ctx - Decoration context
281
+ */
282
+ abstract override buildDecorations(ctx: DecorationContext): void;
283
+ }
284
+
285
+ /**
286
+ * Base class for plugins that add syntax/parser extensions
287
+ * Extends MardoraPlugin with syntax-focused requirements
288
+ */
289
+ export abstract class SyntaxPlugin extends MardoraPlugin {
290
+ /**
291
+ * Subclasses must implement this to provide markdown config
292
+ */
293
+ abstract override getMarkdownConfig(): MarkdownConfig;
294
+ }
@@ -0,0 +1,123 @@
1
+ export type NativeSelectionActivationInput = {
2
+ editorSelectionEmpty: boolean;
3
+ nativeSelectionCollapsed: boolean;
4
+ anchorInEditor: boolean;
5
+ focusInEditor: boolean;
6
+ rangeCount: number;
7
+ anchorExcluded?: boolean;
8
+ focusExcluded?: boolean;
9
+ };
10
+
11
+ export type SelectionToolbarSyntaxNodeInput = {
12
+ selectionFrom: number;
13
+ selectionTo: number;
14
+ nodeFrom: number;
15
+ nodeTo: number;
16
+ nodeName: string;
17
+ };
18
+
19
+ type ClassListLike = {
20
+ length: number;
21
+ item(index: number): string | null;
22
+ };
23
+
24
+ type ElementLike = {
25
+ className?: unknown;
26
+ classList?: unknown;
27
+ parentElement?: unknown;
28
+ };
29
+
30
+ const excludedClassPrefixes = [
31
+ "cm-mardora-code-block",
32
+ "cm-mardora-code-caption",
33
+ "cm-mardora-code-copy",
34
+ "cm-mardora-code-diff",
35
+ "cm-mardora-code-fence",
36
+ "cm-mardora-code-header",
37
+ "cm-mardora-code-line",
38
+ "cm-mardora-image-",
39
+ "cm-mardora-math-",
40
+ "cm-mardora-mermaid-",
41
+ ] as const;
42
+
43
+ const excludedSyntaxNodeNames = new Set(["FencedCode", "MermaidBlock", "MathBlock", "InlineMath", "Image"]);
44
+
45
+ /** Returns whether a browser native selection should activate the toolbar. */
46
+ export function canActivateFromNativeSelection(input: NativeSelectionActivationInput): boolean {
47
+ return (
48
+ !input.nativeSelectionCollapsed &&
49
+ input.anchorInEditor &&
50
+ input.focusInEditor &&
51
+ input.rangeCount > 0 &&
52
+ !input.anchorExcluded &&
53
+ !input.focusExcluded
54
+ );
55
+ }
56
+
57
+ export function hasSelectionToolbarExcludedAncestor(target: unknown, root?: unknown): boolean {
58
+ let current = toElementLike(target);
59
+ while (current) {
60
+ if (hasExcludedClass(current)) {
61
+ return true;
62
+ }
63
+ if (current === root) {
64
+ return false;
65
+ }
66
+ current = toElementLike(current.parentElement);
67
+ }
68
+ return false;
69
+ }
70
+
71
+ export function selectionOverlapsExcludedSyntaxNode(input: SelectionToolbarSyntaxNodeInput): boolean {
72
+ return (
73
+ excludedSyntaxNodeNames.has(input.nodeName) &&
74
+ input.selectionFrom < input.nodeTo &&
75
+ input.selectionTo > input.nodeFrom
76
+ );
77
+ }
78
+
79
+ function toElementLike(value: unknown): ElementLike | null {
80
+ if (!value || typeof value !== "object") {
81
+ return null;
82
+ }
83
+ const element = value as ElementLike;
84
+ if (hasClassData(element)) {
85
+ return element;
86
+ }
87
+ return toElementLike(element.parentElement);
88
+ }
89
+
90
+ function hasClassData(value: ElementLike): boolean {
91
+ return value.className !== undefined || value.classList !== undefined || value.parentElement !== undefined;
92
+ }
93
+
94
+ function hasExcludedClass(element: ElementLike): boolean {
95
+ for (const className of classNamesFrom(element)) {
96
+ if (excludedClassPrefixes.some((prefix) => className.startsWith(prefix))) {
97
+ return true;
98
+ }
99
+ }
100
+ return false;
101
+ }
102
+
103
+ function classNamesFrom(element: ElementLike): string[] {
104
+ const classList = element.classList;
105
+ if (isClassListLike(classList)) {
106
+ const classNames: string[] = [];
107
+ for (let index = 0; index < classList.length; index += 1) {
108
+ const item = classList.item(index);
109
+ if (item) classNames.push(item);
110
+ }
111
+ return classNames;
112
+ }
113
+ return typeof element.className === "string" ? element.className.split(/\s+/).filter(Boolean) : [];
114
+ }
115
+
116
+ function isClassListLike(value: unknown): value is ClassListLike {
117
+ return (
118
+ !!value &&
119
+ typeof value === "object" &&
120
+ typeof (value as ClassListLike).length === "number" &&
121
+ typeof (value as ClassListLike).item === "function"
122
+ );
123
+ }