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/dist/index.cjs +1144 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +257 -0
- package/dist/index.d.ts +257 -0
- package/dist/index.js +1128 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -0
- package/src/draftly.ts +198 -0
- package/src/index.ts +7 -0
- package/src/plugin.ts +245 -0
- package/src/plugins/heading-plugin.ts +163 -0
- package/src/plugins/html-plugin.ts +347 -0
- package/src/plugins/inline-plugin.ts +152 -0
- package/src/plugins/list-plugin.ts +211 -0
- package/src/plugins/plugins.ts +9 -0
- package/src/theme.ts +86 -0
- package/src/utils.ts +21 -0
- package/src/view-plugin.ts +304 -0
package/src/theme.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { EditorView } from "@codemirror/view";
|
|
2
|
+
import { HighlightStyle } from "@codemirror/language";
|
|
3
|
+
|
|
4
|
+
export const highlightStyle = HighlightStyle.define([]);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Base theme for draftly styling
|
|
8
|
+
* Note: Layout styles are scoped under .cm-draftly-enabled which is added by the view plugin
|
|
9
|
+
*/
|
|
10
|
+
export const draftlyBaseTheme = EditorView.baseTheme({
|
|
11
|
+
// Container styles - only apply when view plugin is enabled
|
|
12
|
+
"&.cm-draftly-enabled": {
|
|
13
|
+
fontSize: "16px",
|
|
14
|
+
lineHeight: "1.6",
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
"&.cm-draftly-enabled .cm-content": {
|
|
18
|
+
maxWidth: "48rem",
|
|
19
|
+
margin: "0 auto",
|
|
20
|
+
fontFamily: "var(--font-sans, sans-serif)",
|
|
21
|
+
fontSize: "16px",
|
|
22
|
+
lineHeight: "1.6",
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
// Inline code
|
|
26
|
+
".cm-draftly-inline-code": {
|
|
27
|
+
fontFamily: "var(--font-mono, monospace)",
|
|
28
|
+
fontSize: "0.9em",
|
|
29
|
+
backgroundColor: "rgba(175, 184, 193, 0.2)",
|
|
30
|
+
padding: "0.1em 0.3em",
|
|
31
|
+
borderRadius: "4px",
|
|
32
|
+
},
|
|
33
|
+
".cm-draftly-code-mark": {
|
|
34
|
+
opacity: "0.4",
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
// Links
|
|
38
|
+
".cm-draftly-link": {
|
|
39
|
+
color: "#0969da",
|
|
40
|
+
textDecoration: "none",
|
|
41
|
+
},
|
|
42
|
+
".cm-draftly-link:hover": {
|
|
43
|
+
textDecoration: "underline",
|
|
44
|
+
},
|
|
45
|
+
".cm-draftly-url": {
|
|
46
|
+
opacity: "0.6",
|
|
47
|
+
fontSize: "0.9em",
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
// Images (placeholder styling)
|
|
51
|
+
".cm-draftly-image": {
|
|
52
|
+
color: "#8250df",
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
// Code blocks
|
|
56
|
+
".cm-draftly-fenced-code": {
|
|
57
|
+
fontFamily: "var(--font-mono, monospace)",
|
|
58
|
+
fontSize: "0.9em",
|
|
59
|
+
},
|
|
60
|
+
".cm-draftly-line-code": {
|
|
61
|
+
backgroundColor: "rgba(175, 184, 193, 0.15)",
|
|
62
|
+
borderRadius: "0",
|
|
63
|
+
},
|
|
64
|
+
".cm-draftly-code-info": {
|
|
65
|
+
color: "#6e7781",
|
|
66
|
+
fontStyle: "italic",
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
// Blockquote
|
|
70
|
+
".cm-draftly-line-blockquote": {
|
|
71
|
+
borderLeft: "3px solid #d0d7de",
|
|
72
|
+
paddingLeft: "1em",
|
|
73
|
+
color: "#656d76",
|
|
74
|
+
},
|
|
75
|
+
".cm-draftly-quote-mark": {
|
|
76
|
+
opacity: "0.4",
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
// Horizontal rule
|
|
80
|
+
".cm-draftly-line-hr": {
|
|
81
|
+
textAlign: "center",
|
|
82
|
+
},
|
|
83
|
+
".cm-draftly-hr": {
|
|
84
|
+
opacity: "0.4",
|
|
85
|
+
},
|
|
86
|
+
});
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { EditorView } from "@codemirror/view";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Check if cursor is within the given range
|
|
5
|
+
*/
|
|
6
|
+
export function cursorInRange(view: EditorView, from: number, to: number): boolean {
|
|
7
|
+
const selection = view.state.selection.main;
|
|
8
|
+
return selection.from <= to && selection.to >= from;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Check if any selection overlaps with the given range
|
|
13
|
+
*/
|
|
14
|
+
export function selectionOverlapsRange(view: EditorView, from: number, to: number): boolean {
|
|
15
|
+
for (const range of view.state.selection.ranges) {
|
|
16
|
+
if (range.from <= to && range.to >= from) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { Decoration, DecorationSet, EditorView, ViewPlugin, ViewUpdate } from "@codemirror/view";
|
|
2
|
+
import { Extension, Facet, Range, RangeSetBuilder } from "@codemirror/state";
|
|
3
|
+
import { syntaxHighlighting, syntaxTree } from "@codemirror/language";
|
|
4
|
+
import { cursorInRange, selectionOverlapsRange } from "./utils";
|
|
5
|
+
import { highlightStyle, draftlyBaseTheme } from "./theme";
|
|
6
|
+
import { DecorationContext, DraftlyPlugin } from "./plugin";
|
|
7
|
+
import { DraftlyNode } from "./draftly";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Mark decorations for inline styling
|
|
11
|
+
*/
|
|
12
|
+
const markDecorations = {
|
|
13
|
+
// Inline styles
|
|
14
|
+
"inline-code": Decoration.mark({ class: "cm-draftly-inline-code" }),
|
|
15
|
+
|
|
16
|
+
// Links and images
|
|
17
|
+
link: Decoration.mark({ class: "cm-draftly-link" }),
|
|
18
|
+
"link-text": Decoration.mark({ class: "cm-draftly-link-text" }),
|
|
19
|
+
url: Decoration.mark({ class: "cm-draftly-url" }),
|
|
20
|
+
image: Decoration.mark({ class: "cm-draftly-image" }),
|
|
21
|
+
|
|
22
|
+
// Emphasis markers (* _ ~~ `)
|
|
23
|
+
"emphasis-mark": Decoration.mark({ class: "cm-draftly-emphasis-mark" }),
|
|
24
|
+
|
|
25
|
+
// Code blocks
|
|
26
|
+
"fenced-code": Decoration.mark({ class: "cm-draftly-fenced-code" }),
|
|
27
|
+
"code-mark": Decoration.mark({ class: "cm-draftly-code-mark" }),
|
|
28
|
+
"code-info": Decoration.mark({ class: "cm-draftly-code-info" }),
|
|
29
|
+
|
|
30
|
+
// Blockquote
|
|
31
|
+
blockquote: Decoration.mark({ class: "cm-draftly-blockquote" }),
|
|
32
|
+
"quote-mark": Decoration.mark({ class: "cm-draftly-quote-mark" }),
|
|
33
|
+
|
|
34
|
+
// Horizontal rule
|
|
35
|
+
hr: Decoration.mark({ class: "cm-draftly-hr" }),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Line decorations for block-level elements
|
|
40
|
+
*/
|
|
41
|
+
const lineDecorations = {
|
|
42
|
+
blockquote: Decoration.line({ class: "cm-draftly-line-blockquote" }),
|
|
43
|
+
"code-block": Decoration.line({ class: "cm-draftly-line-code" }),
|
|
44
|
+
hr: Decoration.line({ class: "cm-draftly-line-hr" }),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Facet to register plugins with the view plugin
|
|
49
|
+
*/
|
|
50
|
+
export const DraftlyPluginsFacet = Facet.define<DraftlyPlugin[], DraftlyPlugin[]>({
|
|
51
|
+
combine: (values) => values.flat(),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Facet to register the onNodesChange callback
|
|
56
|
+
*/
|
|
57
|
+
export const draftlyOnNodesChangeFacet = Facet.define<
|
|
58
|
+
((nodes: DraftlyNode[]) => void) | undefined,
|
|
59
|
+
((nodes: DraftlyNode[]) => void) | undefined
|
|
60
|
+
>({
|
|
61
|
+
combine: (values) => values.find((v) => v !== undefined),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Build decorations for the visible viewport
|
|
66
|
+
* @param view - The EditorView instance
|
|
67
|
+
* @param plugins - Optional array of plugins to invoke for decorations
|
|
68
|
+
*/
|
|
69
|
+
function buildDecorations(view: EditorView, plugins: DraftlyPlugin[] = []): DecorationSet {
|
|
70
|
+
const builder = new RangeSetBuilder<Decoration>();
|
|
71
|
+
const decorations: Range<Decoration>[] = [];
|
|
72
|
+
|
|
73
|
+
const tree = syntaxTree(view.state);
|
|
74
|
+
|
|
75
|
+
// Iterate through the syntax tree
|
|
76
|
+
tree.iterate({
|
|
77
|
+
enter: (node) => {
|
|
78
|
+
const { from, to, name } = node;
|
|
79
|
+
|
|
80
|
+
// Skip if cursor is in this range (show raw markdown)
|
|
81
|
+
const cursorInNode = selectionOverlapsRange(view, from, to);
|
|
82
|
+
|
|
83
|
+
// Handle inline code
|
|
84
|
+
if (name === "InlineCode") {
|
|
85
|
+
decorations.push(markDecorations["inline-code"].range(from, to));
|
|
86
|
+
|
|
87
|
+
// Style the backticks
|
|
88
|
+
if (!cursorInNode) {
|
|
89
|
+
const marks = node.node.getChildren("CodeMark");
|
|
90
|
+
for (const mark of marks) {
|
|
91
|
+
decorations.push(markDecorations["code-mark"].range(mark.from, mark.to));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Handle links
|
|
97
|
+
if (name === "Link") {
|
|
98
|
+
decorations.push(markDecorations.link.range(from, to));
|
|
99
|
+
|
|
100
|
+
// Find the URL child
|
|
101
|
+
const url = node.node.getChild("URL");
|
|
102
|
+
if (url) {
|
|
103
|
+
decorations.push(markDecorations.url.range(url.from, url.to));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Handle images
|
|
108
|
+
if (name === "Image") {
|
|
109
|
+
decorations.push(markDecorations.image.range(from, to));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Handle fenced code blocks
|
|
113
|
+
if (name === "FencedCode") {
|
|
114
|
+
decorations.push(markDecorations["fenced-code"].range(from, to));
|
|
115
|
+
|
|
116
|
+
// Add line decorations for each line in the code block
|
|
117
|
+
const startLine = view.state.doc.lineAt(from);
|
|
118
|
+
const endLine = view.state.doc.lineAt(to);
|
|
119
|
+
for (let i = startLine.number; i <= endLine.number; i++) {
|
|
120
|
+
const line = view.state.doc.line(i);
|
|
121
|
+
decorations.push(lineDecorations["code-block"].range(line.from));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Style code info (language identifier)
|
|
125
|
+
const codeInfo = node.node.getChild("CodeInfo");
|
|
126
|
+
if (codeInfo) {
|
|
127
|
+
decorations.push(markDecorations["code-info"].range(codeInfo.from, codeInfo.to));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Style code marks (```)
|
|
131
|
+
const codeMarks = node.node.getChildren("CodeMark");
|
|
132
|
+
for (const mark of codeMarks) {
|
|
133
|
+
decorations.push(markDecorations["code-mark"].range(mark.from, mark.to));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Handle blockquotes
|
|
138
|
+
if (name === "Blockquote") {
|
|
139
|
+
decorations.push(markDecorations.blockquote.range(from, to));
|
|
140
|
+
|
|
141
|
+
// Add line decorations
|
|
142
|
+
const startLine = view.state.doc.lineAt(from);
|
|
143
|
+
const endLine = view.state.doc.lineAt(to);
|
|
144
|
+
for (let i = startLine.number; i <= endLine.number; i++) {
|
|
145
|
+
const line = view.state.doc.line(i);
|
|
146
|
+
decorations.push(lineDecorations.blockquote.range(line.from));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Style quote marks (>)
|
|
150
|
+
const quoteMarks = node.node.getChildren("QuoteMark");
|
|
151
|
+
for (const mark of quoteMarks) {
|
|
152
|
+
decorations.push(markDecorations["quote-mark"].range(mark.from, mark.to));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Handle horizontal rules
|
|
157
|
+
if (name === "HorizontalRule") {
|
|
158
|
+
const line = view.state.doc.lineAt(from);
|
|
159
|
+
decorations.push(lineDecorations.hr.range(line.from));
|
|
160
|
+
decorations.push(markDecorations.hr.range(from, to));
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Allow plugins to contribute decorations
|
|
166
|
+
if (plugins.length > 0) {
|
|
167
|
+
const ctx: DecorationContext = {
|
|
168
|
+
view,
|
|
169
|
+
decorations,
|
|
170
|
+
selectionOverlapsRange: (from, to) => selectionOverlapsRange(view, from, to),
|
|
171
|
+
cursorInRange: (from, to) => cursorInRange(view, from, to),
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Sort plugins by priority and invoke each one's decoration builder
|
|
175
|
+
const sortedPlugins = [...plugins].sort((a, b) => a.decorationPriority - b.decorationPriority);
|
|
176
|
+
|
|
177
|
+
for (const plugin of sortedPlugins) {
|
|
178
|
+
plugin.buildDecorations(ctx);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Sort decorations by position (required for RangeSetBuilder)
|
|
183
|
+
decorations.sort((a, b) => a.from - b.from || a.value.startSide - b.value.startSide);
|
|
184
|
+
|
|
185
|
+
// Build the decoration set
|
|
186
|
+
for (const decoration of decorations) {
|
|
187
|
+
builder.add(decoration.from, decoration.to, decoration.value);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return builder.finish();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* draftly View Plugin
|
|
195
|
+
* Handles rich markdown rendering with decorations
|
|
196
|
+
*/
|
|
197
|
+
class draftlyViewPluginClass {
|
|
198
|
+
decorations: DecorationSet;
|
|
199
|
+
private plugins: DraftlyPlugin[];
|
|
200
|
+
private onNodesChange?: (nodes: DraftlyNode[]) => void;
|
|
201
|
+
|
|
202
|
+
constructor(view: EditorView) {
|
|
203
|
+
this.plugins = view.state.facet(DraftlyPluginsFacet);
|
|
204
|
+
this.onNodesChange = view.state.facet(draftlyOnNodesChangeFacet);
|
|
205
|
+
this.decorations = buildDecorations(view, this.plugins);
|
|
206
|
+
|
|
207
|
+
// Notify plugins that view is ready
|
|
208
|
+
for (const plugin of this.plugins) {
|
|
209
|
+
plugin.onViewReady(view);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Call onNodesChange callback with initial nodes
|
|
213
|
+
if (this.onNodesChange) {
|
|
214
|
+
this.onNodesChange(this.buildNodes(view));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
update(update: ViewUpdate) {
|
|
219
|
+
// Update plugins list if facet changed
|
|
220
|
+
this.plugins = update.view.state.facet(DraftlyPluginsFacet);
|
|
221
|
+
this.onNodesChange = update.view.state.facet(draftlyOnNodesChangeFacet);
|
|
222
|
+
|
|
223
|
+
// Notify plugins of the update
|
|
224
|
+
for (const plugin of this.plugins) {
|
|
225
|
+
plugin.onViewUpdate(update);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Rebuild decorations when:
|
|
229
|
+
// - Document changes
|
|
230
|
+
// - Selection changes (to show/hide syntax markers)
|
|
231
|
+
// - Viewport changes
|
|
232
|
+
if (update.docChanged || update.selectionSet || update.viewportChanged) {
|
|
233
|
+
this.decorations = buildDecorations(update.view, this.plugins);
|
|
234
|
+
|
|
235
|
+
// Call onNodesChange callback
|
|
236
|
+
if (this.onNodesChange) {
|
|
237
|
+
this.onNodesChange(this.buildNodes(update.view));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private buildNodes(view: EditorView): DraftlyNode[] {
|
|
243
|
+
const tree = syntaxTree(view.state);
|
|
244
|
+
const roots: DraftlyNode[] = [];
|
|
245
|
+
const stack: DraftlyNode[] = [];
|
|
246
|
+
|
|
247
|
+
tree.iterate({
|
|
248
|
+
enter: (nodeRef) => {
|
|
249
|
+
const node: DraftlyNode = {
|
|
250
|
+
from: nodeRef.from,
|
|
251
|
+
to: nodeRef.to,
|
|
252
|
+
name: nodeRef.name,
|
|
253
|
+
children: [],
|
|
254
|
+
isSelected: selectionOverlapsRange(view, nodeRef.from, nodeRef.to),
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
if (stack.length > 0) {
|
|
258
|
+
stack[stack.length - 1]!.children.push(node);
|
|
259
|
+
} else {
|
|
260
|
+
roots.push(node);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
stack.push(node);
|
|
264
|
+
},
|
|
265
|
+
leave: () => {
|
|
266
|
+
stack.pop();
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
return roots;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* The main draftly ViewPlugin extension
|
|
276
|
+
*/
|
|
277
|
+
export const draftlyViewPlugin = ViewPlugin.fromClass(draftlyViewPluginClass, {
|
|
278
|
+
decorations: (v) => v.decorations,
|
|
279
|
+
provide: () => [syntaxHighlighting(highlightStyle)],
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Extension to add the cm-draftly-enabled class to the editor
|
|
284
|
+
*/
|
|
285
|
+
const draftlyEditorClass = EditorView.editorAttributes.of({ class: "cm-draftly-enabled" });
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Create draftly view extension bundle with plugin support
|
|
289
|
+
* @param plugins - Optional array of DraftlyPlugin instances
|
|
290
|
+
* @param onNodesChange - Optional callback to receive nodes on every update
|
|
291
|
+
* @returns Extension array including view plugin, theme, and plugin facet
|
|
292
|
+
*/
|
|
293
|
+
export function createDraftlyViewExtension(
|
|
294
|
+
plugins: DraftlyPlugin[] = [],
|
|
295
|
+
onNodesChange?: (nodes: DraftlyNode[]) => void
|
|
296
|
+
): Extension[] {
|
|
297
|
+
return [
|
|
298
|
+
DraftlyPluginsFacet.of(plugins),
|
|
299
|
+
draftlyOnNodesChangeFacet.of(onNodesChange),
|
|
300
|
+
draftlyViewPlugin,
|
|
301
|
+
draftlyBaseTheme,
|
|
302
|
+
draftlyEditorClass,
|
|
303
|
+
];
|
|
304
|
+
}
|