draftly 0.1.0-alpha.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.
package/src/plugin.ts ADDED
@@ -0,0 +1,245 @@
1
+ import { Decoration, EditorView, KeyBinding, ViewUpdate, WidgetType } from "@codemirror/view";
2
+ import { Extension, Range } from "@codemirror/state";
3
+ import { MarkdownConfig } from "@lezer/markdown";
4
+ import { DraftlyConfig } from "./draftly";
5
+
6
+ /**
7
+ * Context passed to plugin lifecycle methods
8
+ */
9
+ export interface PluginContext {
10
+ /** Current configuration */
11
+ readonly config: DraftlyConfig;
12
+ }
13
+
14
+ /**
15
+ * Plugin configuration schema
16
+ */
17
+ export interface PluginConfig {
18
+ [key: string]: unknown;
19
+ }
20
+
21
+ /**
22
+ * Decoration context passed to plugin decoration builders
23
+ * Provides access to view state and decoration collection
24
+ */
25
+ export interface DecorationContext {
26
+ /** The EditorView instance (readonly) */
27
+ readonly view: EditorView;
28
+
29
+ /** Array to push decorations into (will be sorted automatically) */
30
+ readonly decorations: Range<Decoration>[];
31
+
32
+ /** Check if selection overlaps with a range (to show raw markdown) */
33
+ selectionOverlapsRange(from: number, to: number): boolean;
34
+
35
+ /** Check if cursor is within a range */
36
+ cursorInRange(from: number, to: number): boolean;
37
+ }
38
+
39
+ /**
40
+ * Abstract base class for all draftly plugins
41
+ *
42
+ * Implements OOP principles:
43
+ * - Abstraction: abstract name/version must be implemented by subclasses
44
+ * - Encapsulation: private _config, protected _context
45
+ * - Inheritance: specialized plugin classes can extend this
46
+ */
47
+ export abstract class DraftlyPlugin {
48
+ /** Unique plugin identifier (abstract - must be implemented) */
49
+ abstract readonly name: string;
50
+
51
+ /** Plugin version (abstract - must be implemented) */
52
+ abstract readonly version: string;
53
+
54
+ /** Plugin dependencies - names of required plugins */
55
+ readonly dependencies: string[] = [];
56
+
57
+ /** Private configuration storage */
58
+ private _config: PluginConfig = {};
59
+
60
+ /** Protected context - accessible to subclasses */
61
+ protected _context: PluginContext | null = null;
62
+
63
+ /** Get plugin configuration */
64
+ get config(): PluginConfig {
65
+ return this._config;
66
+ }
67
+
68
+ /** Set plugin configuration */
69
+ set config(value: PluginConfig) {
70
+ this._config = value;
71
+ }
72
+
73
+ /** Get plugin context */
74
+ get context(): PluginContext | null {
75
+ return this._context;
76
+ }
77
+
78
+ // ============================================
79
+ // EXTENSION METHODS (overridable by subclasses)
80
+ // ============================================
81
+
82
+ /**
83
+ * Return CodeMirror extensions for this plugin
84
+ * Override to provide custom extensions
85
+ */
86
+ getExtensions(): Extension[] {
87
+ return [];
88
+ }
89
+
90
+ /**
91
+ * Return markdown parser extensions
92
+ * Override to extend markdown parsing
93
+ */
94
+ getMarkdownConfig(): MarkdownConfig | null {
95
+ return null;
96
+ }
97
+
98
+ /**
99
+ * Return keybindings for this plugin
100
+ * Override to add custom keyboard shortcuts
101
+ */
102
+ getKeymap(): KeyBinding[] {
103
+ return [];
104
+ }
105
+
106
+ // ============================================
107
+ // DECORATION METHODS (overridable by subclasses)
108
+ // ============================================
109
+
110
+ /**
111
+ * Decoration priority (higher = applied later)
112
+ * Override to customize priority. Default: 100
113
+ */
114
+ get decorationPriority(): number {
115
+ return 100;
116
+ }
117
+
118
+ /**
119
+ * Build decorations for the current view state
120
+ * Override to contribute decorations to the editor
121
+ *
122
+ * @param ctx - Decoration context with view and decoration array
123
+ */
124
+ buildDecorations(_ctx: DecorationContext): void {
125
+ // Default implementation does nothing
126
+ // Subclasses override to add decorations
127
+ }
128
+
129
+ // ============================================
130
+ // LIFECYCLE HOOKS (overridable by subclasses)
131
+ // ============================================
132
+
133
+ /**
134
+ * Called when plugin is registered with draftly
135
+ * Override to perform initialization
136
+ *
137
+ * @param context - Plugin context with configuration
138
+ */
139
+ onRegister(context: PluginContext): void | Promise<void> {
140
+ this._context = context;
141
+ }
142
+
143
+ /**
144
+ * Called when plugin is unregistered
145
+ * Override to perform cleanup
146
+ */
147
+ onUnregister(): void {
148
+ this._context = null;
149
+ }
150
+
151
+ /**
152
+ * Called when EditorView is created and ready
153
+ * Override to perform view-specific initialization
154
+ *
155
+ * @param view - The EditorView instance
156
+ */
157
+ onViewReady(_view: EditorView): void {
158
+ // Default implementation does nothing
159
+ }
160
+
161
+ /**
162
+ * Called on view updates (document changes, selection changes, etc.)
163
+ * Override to react to editor changes
164
+ *
165
+ * @param update - The ViewUpdate with change information
166
+ */
167
+ onViewUpdate(_update: ViewUpdate): void {
168
+ // Default implementation does nothing
169
+ }
170
+
171
+ // ============================================
172
+ // ACCESSIBILITY METHODS (overridable by subclasses)
173
+ // ============================================
174
+
175
+ /**
176
+ * Return ARIA attributes to add to the editor
177
+ * Override to improve accessibility
178
+ */
179
+ getAriaAttributes(): Record<string, string> {
180
+ return {};
181
+ }
182
+
183
+ // ============================================
184
+ // VIEW CONTRIBUTIONS (overridable by subclasses)
185
+ // ============================================
186
+
187
+ /**
188
+ * Return widget types this plugin provides
189
+ * Override to contribute custom widgets
190
+ */
191
+ getWidgets(): WidgetType[] {
192
+ return [];
193
+ }
194
+
195
+ // ============================================
196
+ // PROTECTED UTILITIES (for subclasses)
197
+ // ============================================
198
+
199
+ /**
200
+ * Helper to get current editor state
201
+ * @param view - The EditorView instance
202
+ */
203
+ protected getState(view: EditorView) {
204
+ return view.state;
205
+ }
206
+
207
+ /**
208
+ * Helper to get current document
209
+ * @param view - The EditorView instance
210
+ */
211
+ protected getDocument(view: EditorView) {
212
+ return view.state.doc;
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Base class for plugins that primarily contribute decorations
218
+ * Extends DraftlyPlugin with decoration-focused defaults
219
+ */
220
+ export abstract class DecorationPlugin extends DraftlyPlugin {
221
+ /**
222
+ * Decoration priority - lower than default for decoration plugins
223
+ * Override to customize
224
+ */
225
+ override get decorationPriority(): number {
226
+ return 50;
227
+ }
228
+
229
+ /**
230
+ * Subclasses must implement this to provide decorations
231
+ * @param ctx - Decoration context
232
+ */
233
+ abstract override buildDecorations(ctx: DecorationContext): void;
234
+ }
235
+
236
+ /**
237
+ * Base class for plugins that add syntax/parser extensions
238
+ * Extends DraftlyPlugin with syntax-focused requirements
239
+ */
240
+ export abstract class SyntaxPlugin extends DraftlyPlugin {
241
+ /**
242
+ * Subclasses must implement this to provide markdown config
243
+ */
244
+ abstract override getMarkdownConfig(): MarkdownConfig;
245
+ }
@@ -0,0 +1,163 @@
1
+ import { Decoration, EditorView } from "@codemirror/view";
2
+ import { syntaxTree } from "@codemirror/language";
3
+ import { DecorationContext, DecorationPlugin } from "../plugin";
4
+ import { Extension } from "@codemirror/state";
5
+
6
+ /**
7
+ * Node types for ATX headings in markdown
8
+ */
9
+ const HEADING_TYPES = ["ATXHeading1", "ATXHeading2", "ATXHeading3", "ATXHeading4", "ATXHeading5", "ATXHeading6"];
10
+
11
+ /**
12
+ * Mark decorations for heading content
13
+ */
14
+ const headingMarkDecorations = {
15
+ "heading-1": Decoration.mark({ class: "cm-draftly-h1" }),
16
+ "heading-2": Decoration.mark({ class: "cm-draftly-h2" }),
17
+ "heading-3": Decoration.mark({ class: "cm-draftly-h3" }),
18
+ "heading-4": Decoration.mark({ class: "cm-draftly-h4" }),
19
+ "heading-5": Decoration.mark({ class: "cm-draftly-h5" }),
20
+ "heading-6": Decoration.mark({ class: "cm-draftly-h6" }),
21
+ "heading-mark": Decoration.mark({ class: "cm-draftly-heading-mark" }),
22
+ };
23
+
24
+ /**
25
+ * Line decorations for heading lines
26
+ */
27
+ const headingLineDecorations = {
28
+ "heading-1": Decoration.line({ class: "cm-draftly-line-h1" }),
29
+ "heading-2": Decoration.line({ class: "cm-draftly-line-h2" }),
30
+ "heading-3": Decoration.line({ class: "cm-draftly-line-h3" }),
31
+ "heading-4": Decoration.line({ class: "cm-draftly-line-h4" }),
32
+ "heading-5": Decoration.line({ class: "cm-draftly-line-h5" }),
33
+ "heading-6": Decoration.line({ class: "cm-draftly-line-h6" }),
34
+ };
35
+
36
+ /**
37
+ * HeadingPlugin - Decorates markdown headings
38
+ *
39
+ * Adds visual styling to ATX headings (# through ######)
40
+ * - Line decorations for the entire heading line
41
+ * - Mark decorations for heading content
42
+ * - Hides # markers when cursor is not in the heading
43
+ */
44
+ export class HeadingPlugin extends DecorationPlugin {
45
+ readonly name = "heading";
46
+ readonly version = "1.0.0";
47
+
48
+ /**
49
+ * Higher priority to ensure headings are styled first
50
+ */
51
+ override get decorationPriority(): number {
52
+ return 10;
53
+ }
54
+
55
+ /**
56
+ * Get the extensions for this plugin
57
+ */
58
+ getExtensions(): Extension[] {
59
+ return [headingTheme];
60
+ }
61
+
62
+ /**
63
+ * Build heading decorations by iterating the syntax tree
64
+ */
65
+ override buildDecorations(ctx: DecorationContext): void {
66
+ const { view, decorations } = ctx;
67
+ const tree = syntaxTree(view.state);
68
+
69
+ tree.iterate({
70
+ enter: (node) => {
71
+ const { from, to, name } = node;
72
+
73
+ if (!HEADING_TYPES.includes(name)) {
74
+ return;
75
+ }
76
+
77
+ const level = parseInt(name.slice(-1), 10);
78
+ const headingClass = `heading-${level}` as keyof typeof headingMarkDecorations;
79
+ const lineClass = `heading-${level}` as keyof typeof headingLineDecorations;
80
+
81
+ // Add line decoration
82
+ const line = view.state.doc.lineAt(from);
83
+ decorations.push(headingLineDecorations[lineClass].range(line.from));
84
+
85
+ // Add mark decoration for the heading content
86
+ decorations.push(headingMarkDecorations[headingClass].range(from, to + 1));
87
+
88
+ // Find and style the heading marker (#)
89
+ // Only hide when cursor is not in the heading
90
+ const cursorInNode = ctx.selectionOverlapsRange(from, to);
91
+ if (!cursorInNode) {
92
+ const headingMark = node.node.getChild("HeaderMark");
93
+ if (headingMark) {
94
+ decorations.push(headingMarkDecorations["heading-mark"].range(headingMark.from, headingMark.to + 1));
95
+ }
96
+ }
97
+ },
98
+ });
99
+ }
100
+ }
101
+
102
+ const headingTheme = EditorView.theme({
103
+ ".cm-draftly-h1": {
104
+ fontSize: "2em",
105
+ fontWeight: "bold",
106
+ fontFamily: "sans-serif",
107
+ textDecoration: "none",
108
+ },
109
+
110
+ ".cm-draftly-h2": {
111
+ fontSize: "1.75em",
112
+ fontWeight: "bold",
113
+ fontFamily: "sans-serif",
114
+ textDecoration: "none",
115
+ },
116
+
117
+ ".cm-draftly-h3": {
118
+ fontSize: "1.5em",
119
+ fontWeight: "bold",
120
+ fontFamily: "sans-serif",
121
+ textDecoration: "none",
122
+ },
123
+
124
+ ".cm-draftly-h4": {
125
+ fontSize: "1.25em",
126
+ fontWeight: "bold",
127
+ fontFamily: "sans-serif",
128
+ textDecoration: "none",
129
+ },
130
+
131
+ ".cm-draftly-h5": {
132
+ fontSize: "1em",
133
+ fontWeight: "bold",
134
+ fontFamily: "sans-serif",
135
+ textDecoration: "none",
136
+ },
137
+
138
+ ".cm-draftly-h6": {
139
+ fontSize: "0.75em",
140
+ fontWeight: "bold",
141
+ fontFamily: "sans-serif",
142
+ textDecoration: "none",
143
+ },
144
+
145
+ // Heading line styles
146
+ ".cm-draftly-line-h1": {
147
+ paddingTop: "1.5em",
148
+ paddingBottom: "0.5em",
149
+ },
150
+ ".cm-draftly-line-h2": {
151
+ paddingTop: "1.25em",
152
+ paddingBottom: "0.5em",
153
+ },
154
+ ".cm-draftly-line-h3, .cm-draftly-line-h4, .cm-draftly-line-h5, .cm-draftly-line-h6": {
155
+ paddingTop: "1em",
156
+ paddingBottom: "0.5em",
157
+ },
158
+
159
+ // Heading mark (# symbols)
160
+ ".cm-draftly-heading-mark": {
161
+ display: "none",
162
+ },
163
+ });