draftly 0.1.0-alpha.0 → 1.0.0-alpha.2
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/README.md +346 -0
- package/dist/chunk-2B3A3VSQ.cjs +3382 -0
- package/dist/chunk-2B3A3VSQ.cjs.map +1 -0
- package/dist/chunk-72ZYRGRT.cjs +399 -0
- package/dist/chunk-72ZYRGRT.cjs.map +1 -0
- package/dist/chunk-CG4M4TC7.js +392 -0
- package/dist/chunk-CG4M4TC7.js.map +1 -0
- package/dist/chunk-DFQYXFOP.js +86 -0
- package/dist/chunk-DFQYXFOP.js.map +1 -0
- package/dist/chunk-HPSMS2WB.js +182 -0
- package/dist/chunk-HPSMS2WB.js.map +1 -0
- package/dist/chunk-KBQDZ5IW.cjs +192 -0
- package/dist/chunk-KBQDZ5IW.cjs.map +1 -0
- package/dist/chunk-KDEDLC3D.cjs +93 -0
- package/dist/chunk-KDEDLC3D.cjs.map +1 -0
- package/dist/chunk-N3WL3XPB.js +3360 -0
- package/dist/chunk-N3WL3XPB.js.map +1 -0
- package/dist/draftly-BLnx3uGX.d.cts +293 -0
- package/dist/draftly-BLnx3uGX.d.ts +293 -0
- package/dist/editor/index.cjs +57 -0
- package/dist/editor/index.cjs.map +1 -0
- package/dist/editor/index.d.cts +15 -0
- package/dist/editor/index.d.ts +15 -0
- package/dist/editor/index.js +4 -0
- package/dist/editor/index.js.map +1 -0
- package/dist/index.cjs +120 -1129
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -257
- package/dist/index.d.ts +9 -257
- package/dist/index.js +4 -1126
- package/dist/index.js.map +1 -1
- package/dist/plugins/index.cjs +66 -0
- package/dist/plugins/index.cjs.map +1 -0
- package/dist/plugins/index.d.cts +515 -0
- package/dist/plugins/index.d.ts +515 -0
- package/dist/plugins/index.js +5 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/preview/index.cjs +29 -0
- package/dist/preview/index.cjs.map +1 -0
- package/dist/preview/index.d.cts +143 -0
- package/dist/preview/index.d.ts +143 -0
- package/dist/preview/index.js +4 -0
- package/dist/preview/index.js.map +1 -0
- package/package.json +22 -1
- package/src/{draftly.ts → editor/draftly.ts} +28 -27
- package/src/editor/index.ts +5 -0
- package/src/{plugin.ts → editor/plugin.ts} +62 -34
- package/src/editor/theme.ts +62 -0
- package/src/editor/utils.ts +143 -0
- package/src/{view-plugin.ts → editor/view-plugin.ts} +25 -140
- package/src/index.ts +4 -7
- package/src/plugins/code-plugin.ts +1119 -0
- package/src/plugins/heading-plugin.ts +108 -74
- package/src/plugins/hr-plugin.ts +102 -0
- package/src/plugins/html-plugin.ts +59 -53
- package/src/plugins/image-plugin.ts +447 -0
- package/src/plugins/index.ts +57 -0
- package/src/plugins/inline-plugin.ts +178 -39
- package/src/plugins/link-plugin.ts +509 -0
- package/src/plugins/list-plugin.ts +492 -211
- package/src/plugins/math-plugin.ts +514 -0
- package/src/plugins/mermaid-plugin.ts +500 -0
- package/src/plugins/paragraph-plugin.ts +38 -0
- package/src/plugins/quote-plugin.ts +146 -0
- package/src/preview/context.ts +38 -0
- package/src/preview/css-generator.ts +51 -0
- package/src/preview/default-renderers.ts +29 -0
- package/src/preview/index.ts +20 -0
- package/src/preview/preview.ts +40 -0
- package/src/preview/renderer.ts +157 -0
- package/src/preview/types.ts +72 -0
- package/src/plugins/plugins.ts +0 -9
- package/src/theme.ts +0 -86
- package/src/utils.ts +0 -21
|
@@ -1,211 +1,492 @@
|
|
|
1
|
-
import { Decoration, EditorView, WidgetType } from "@codemirror/view";
|
|
2
|
-
import { syntaxTree } from "@codemirror/language";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
wrap.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
1
|
+
import { Decoration, EditorView, KeyBinding, WidgetType } from "@codemirror/view";
|
|
2
|
+
import { syntaxTree } from "@codemirror/language";
|
|
3
|
+
import { DecorationContext, DecorationPlugin } from "../editor/plugin";
|
|
4
|
+
import { createTheme } from "../editor";
|
|
5
|
+
import { Range } from "@codemirror/state";
|
|
6
|
+
import { SyntaxNode } from "@lezer/common";
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// CSS Classes
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
const classes = {
|
|
13
|
+
// Unordered list classes
|
|
14
|
+
lineUL: "cm-draftly-list-line-ul",
|
|
15
|
+
markUL: "cm-draftly-list-mark-ul",
|
|
16
|
+
|
|
17
|
+
// Ordered list classes
|
|
18
|
+
lineOL: "cm-draftly-list-line-ol",
|
|
19
|
+
markOL: "cm-draftly-list-mark-ol",
|
|
20
|
+
|
|
21
|
+
// Task list classes
|
|
22
|
+
taskLine: "cm-draftly-task-line",
|
|
23
|
+
taskMarker: "cm-draftly-task-marker",
|
|
24
|
+
|
|
25
|
+
// Common classes
|
|
26
|
+
content: "cm-draftly-list-content",
|
|
27
|
+
indent: "cm-draftly-list-indent",
|
|
28
|
+
active: " cm-draftly-active",
|
|
29
|
+
preview: "cm-draftly-preview",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// Checkbox Widget
|
|
34
|
+
// ============================================================================
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Interactive checkbox widget for task list items.
|
|
38
|
+
* Replaces `[ ]` or `[x]` markers with a clickable checkbox when not editing.
|
|
39
|
+
*/
|
|
40
|
+
export class TaskCheckboxWidget extends WidgetType {
|
|
41
|
+
constructor(readonly checked: boolean) {
|
|
42
|
+
super();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
override eq(other: TaskCheckboxWidget): boolean {
|
|
46
|
+
return other.checked === this.checked;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
toDOM(view: EditorView): HTMLElement {
|
|
50
|
+
const wrap = document.createElement("span");
|
|
51
|
+
wrap.className = `cm-draftly-task-checkbox ${this.checked ? "checked" : ""}`;
|
|
52
|
+
wrap.setAttribute("aria-hidden", "true");
|
|
53
|
+
|
|
54
|
+
const checkbox = document.createElement("input");
|
|
55
|
+
checkbox.type = "checkbox";
|
|
56
|
+
checkbox.checked = this.checked;
|
|
57
|
+
checkbox.tabIndex = -1;
|
|
58
|
+
|
|
59
|
+
checkbox.addEventListener("mousedown", (e) => {
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
this.toggleCheckbox(view, wrap);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
wrap.appendChild(checkbox);
|
|
65
|
+
return wrap;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
override ignoreEvent(): boolean {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Toggle the checkbox state in the document */
|
|
73
|
+
private toggleCheckbox(view: EditorView, wrap: HTMLElement): void {
|
|
74
|
+
const pos = view.posAtDOM(wrap);
|
|
75
|
+
const line = view.state.doc.lineAt(pos);
|
|
76
|
+
const match = line.text.match(/^(\s*(?:[-*+]|\d+\.)\s*)\[([ xX])\]/);
|
|
77
|
+
|
|
78
|
+
if (match) {
|
|
79
|
+
const markerStart = line.from + match[1]!.length + 1;
|
|
80
|
+
const newChar = this.checked ? " " : "x";
|
|
81
|
+
view.dispatch({
|
|
82
|
+
changes: { from: markerStart, to: markerStart + 1, insert: newChar },
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ============================================================================
|
|
89
|
+
// List Plugin
|
|
90
|
+
// ============================================================================
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Decorates markdown lists with custom styling.
|
|
94
|
+
*
|
|
95
|
+
* Supports:
|
|
96
|
+
* - **Unordered lists** — Replaces `*`, `-`, `+` markers with styled bullets
|
|
97
|
+
* - **Ordered lists** — Styles numbered markers (`1.`, `2.`, etc.)
|
|
98
|
+
* - **Task lists** — Renders `[ ]`/`[x]` as interactive checkboxes
|
|
99
|
+
*/
|
|
100
|
+
export class ListPlugin extends DecorationPlugin {
|
|
101
|
+
readonly name = "list";
|
|
102
|
+
readonly version = "1.0.0";
|
|
103
|
+
override decorationPriority = 20;
|
|
104
|
+
override readonly requiredNodes = [
|
|
105
|
+
"BulletList",
|
|
106
|
+
"OrderedList",
|
|
107
|
+
"ListItem",
|
|
108
|
+
"ListMark",
|
|
109
|
+
"Task",
|
|
110
|
+
"TaskMarker",
|
|
111
|
+
] as const;
|
|
112
|
+
|
|
113
|
+
override get theme() {
|
|
114
|
+
return theme;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Keyboard shortcuts for list formatting
|
|
119
|
+
*/
|
|
120
|
+
override getKeymap(): KeyBinding[] {
|
|
121
|
+
return [
|
|
122
|
+
{
|
|
123
|
+
key: "Mod-Shift-8",
|
|
124
|
+
run: (view) => this.toggleListOnLines(view, "- "),
|
|
125
|
+
preventDefault: true,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
key: "Mod-Shift-7",
|
|
129
|
+
run: (view) => this.toggleListOnLines(view, "1. "),
|
|
130
|
+
preventDefault: true,
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
key: "Mod-Shift-9",
|
|
134
|
+
run: (view) => this.toggleListOnLines(view, "- [ ] "),
|
|
135
|
+
preventDefault: true,
|
|
136
|
+
},
|
|
137
|
+
];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Toggle list marker on current line or selected lines
|
|
142
|
+
*/
|
|
143
|
+
private toggleListOnLines(view: EditorView, marker: string): boolean {
|
|
144
|
+
const { state } = view;
|
|
145
|
+
const { from, to } = state.selection.main;
|
|
146
|
+
|
|
147
|
+
// Get all lines in selection
|
|
148
|
+
const startLine = state.doc.lineAt(from);
|
|
149
|
+
const endLine = state.doc.lineAt(to);
|
|
150
|
+
|
|
151
|
+
const changes: { from: number; to: number; insert: string }[] = [];
|
|
152
|
+
|
|
153
|
+
// Regex to match existing list markers
|
|
154
|
+
const listMarkerRegex = /^(\s*)([-*+]|\d+\.)\s(\[[ xX]\]\s)?/;
|
|
155
|
+
|
|
156
|
+
const isOrderedMarker = marker === "1. ";
|
|
157
|
+
let orderNum = 1;
|
|
158
|
+
|
|
159
|
+
for (let lineNum = startLine.number; lineNum <= endLine.number; lineNum++) {
|
|
160
|
+
const line = state.doc.line(lineNum);
|
|
161
|
+
const match = line.text.match(listMarkerRegex);
|
|
162
|
+
|
|
163
|
+
// Get the actual marker to insert (incremental for ordered lists)
|
|
164
|
+
const actualMarker = isOrderedMarker ? `${orderNum}. ` : marker;
|
|
165
|
+
|
|
166
|
+
if (match) {
|
|
167
|
+
// Line already has a list marker - check if same type
|
|
168
|
+
const existingMarker = match[0];
|
|
169
|
+
const indent = match[1] || "";
|
|
170
|
+
|
|
171
|
+
// Check if this is the same marker type (toggle off)
|
|
172
|
+
const isUnordered = /^[-*+]$/.test(match[2]!);
|
|
173
|
+
const isOrdered = /^\d+\.$/.test(match[2]!);
|
|
174
|
+
const hasTask = !!match[3];
|
|
175
|
+
|
|
176
|
+
const wantUnordered = marker === "- ";
|
|
177
|
+
const wantOrdered = isOrderedMarker;
|
|
178
|
+
const wantTask = marker === "- [ ] ";
|
|
179
|
+
|
|
180
|
+
if (
|
|
181
|
+
(wantUnordered && isUnordered && !hasTask) ||
|
|
182
|
+
(wantOrdered && isOrdered && !hasTask) ||
|
|
183
|
+
(wantTask && hasTask)
|
|
184
|
+
) {
|
|
185
|
+
// Same type - remove the marker
|
|
186
|
+
changes.push({
|
|
187
|
+
from: line.from,
|
|
188
|
+
to: line.from + existingMarker.length,
|
|
189
|
+
insert: indent,
|
|
190
|
+
});
|
|
191
|
+
} else {
|
|
192
|
+
// Different type - replace the marker
|
|
193
|
+
changes.push({
|
|
194
|
+
from: line.from,
|
|
195
|
+
to: line.from + existingMarker.length,
|
|
196
|
+
insert: indent + actualMarker,
|
|
197
|
+
});
|
|
198
|
+
orderNum++;
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
// No list marker - add one at start of line (after any indent)
|
|
202
|
+
const indentMatch = line.text.match(/^(\s*)/);
|
|
203
|
+
const indent = indentMatch ? indentMatch[1]! : "";
|
|
204
|
+
changes.push({
|
|
205
|
+
from: line.from + indent.length,
|
|
206
|
+
to: line.from + indent.length,
|
|
207
|
+
insert: actualMarker,
|
|
208
|
+
});
|
|
209
|
+
orderNum++;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (changes.length > 0) {
|
|
214
|
+
view.dispatch({ changes });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
buildDecorations(ctx: DecorationContext): void {
|
|
221
|
+
const { view, decorations } = ctx;
|
|
222
|
+
const tree = syntaxTree(view.state);
|
|
223
|
+
|
|
224
|
+
tree.iterate({
|
|
225
|
+
enter: (node) => {
|
|
226
|
+
const { from, to, name } = node;
|
|
227
|
+
const line = view.state.doc.lineAt(from);
|
|
228
|
+
const cursorInLine = ctx.cursorInRange(line.from, line.to);
|
|
229
|
+
|
|
230
|
+
switch (name) {
|
|
231
|
+
case "ListItem":
|
|
232
|
+
this.decorateListItem(node, line, decorations);
|
|
233
|
+
break;
|
|
234
|
+
|
|
235
|
+
case "ListMark":
|
|
236
|
+
this.decorateListMark(node, line, decorations, cursorInLine);
|
|
237
|
+
break;
|
|
238
|
+
|
|
239
|
+
case "TaskMarker":
|
|
240
|
+
this.decorateTaskMarker(from, to, view, decorations, cursorInLine);
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Add line decoration for list items with nesting depth */
|
|
248
|
+
private decorateListItem(
|
|
249
|
+
node: Parameters<NonNullable<Parameters<ReturnType<typeof syntaxTree>["iterate"]>[0]["enter"]>>[0],
|
|
250
|
+
line: { from: number },
|
|
251
|
+
decorations: Range<Decoration>[]
|
|
252
|
+
): void {
|
|
253
|
+
const parent = node.node.parent;
|
|
254
|
+
const listType = parent?.name;
|
|
255
|
+
|
|
256
|
+
// Calculate nesting depth
|
|
257
|
+
let depth = 0;
|
|
258
|
+
let ancestor = node.node.parent;
|
|
259
|
+
while (ancestor) {
|
|
260
|
+
if (ancestor.name === "ListItem") depth++;
|
|
261
|
+
ancestor = ancestor.parent;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Check for task marker child
|
|
265
|
+
const hasTask = this.hasTaskChild(node);
|
|
266
|
+
|
|
267
|
+
// Determine line class based on list type
|
|
268
|
+
let lineClass: string;
|
|
269
|
+
if (hasTask) lineClass = classes.taskLine;
|
|
270
|
+
else if (listType === "OrderedList") lineClass = classes.lineOL;
|
|
271
|
+
else lineClass = classes.lineUL;
|
|
272
|
+
|
|
273
|
+
decorations.push(
|
|
274
|
+
Decoration.line({
|
|
275
|
+
class: lineClass,
|
|
276
|
+
attributes: { style: `--depth: ${depth}` },
|
|
277
|
+
}).range(line.from)
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Check if a ListItem node has a Task child */
|
|
282
|
+
private hasTaskChild(
|
|
283
|
+
node: Parameters<NonNullable<Parameters<ReturnType<typeof syntaxTree>["iterate"]>[0]["enter"]>>[0]
|
|
284
|
+
): boolean {
|
|
285
|
+
const cursor = node.node.cursor();
|
|
286
|
+
if (cursor.firstChild()) {
|
|
287
|
+
do {
|
|
288
|
+
if (cursor.name === "Task") return true;
|
|
289
|
+
} while (cursor.nextSibling());
|
|
290
|
+
}
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Decorate list markers (bullets for UL, numbers for OL) */
|
|
295
|
+
private decorateListMark(
|
|
296
|
+
node: Parameters<NonNullable<Parameters<ReturnType<typeof syntaxTree>["iterate"]>[0]["enter"]>>[0],
|
|
297
|
+
line: { from: number; to: number },
|
|
298
|
+
decorations: Range<Decoration>[],
|
|
299
|
+
cursorInLine: boolean
|
|
300
|
+
): void {
|
|
301
|
+
const { from, to } = node;
|
|
302
|
+
const parent = node.node.parent;
|
|
303
|
+
const grandparent = parent?.parent;
|
|
304
|
+
const listType = grandparent?.name;
|
|
305
|
+
const activeClass = cursorInLine ? classes.active : "";
|
|
306
|
+
|
|
307
|
+
// Add indent decoration for nested items
|
|
308
|
+
if (from > line.from) {
|
|
309
|
+
decorations.push(Decoration.mark({ class: classes.indent + activeClass }).range(line.from, from));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Add marker decoration based on list type
|
|
313
|
+
const markClass = listType === "OrderedList" ? classes.markOL : classes.markUL;
|
|
314
|
+
decorations.push(Decoration.mark({ class: markClass + activeClass }).range(from, to + 1));
|
|
315
|
+
|
|
316
|
+
// Wrap remaining line content
|
|
317
|
+
const contentStart = to + 1;
|
|
318
|
+
if (contentStart < line.to) {
|
|
319
|
+
decorations.push(Decoration.mark({ class: classes.content }).range(contentStart, line.to));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Decorate task markers - show checkbox widget or raw text based on cursor */
|
|
324
|
+
private decorateTaskMarker(
|
|
325
|
+
from: number,
|
|
326
|
+
to: number,
|
|
327
|
+
view: EditorView,
|
|
328
|
+
decorations: Range<Decoration>[],
|
|
329
|
+
cursorInLine: boolean
|
|
330
|
+
): void {
|
|
331
|
+
const text = view.state.sliceDoc(from, to);
|
|
332
|
+
const isChecked = text.includes("x") || text.includes("X");
|
|
333
|
+
|
|
334
|
+
if (cursorInLine) {
|
|
335
|
+
// Show raw marker when editing
|
|
336
|
+
decorations.push(Decoration.mark({ class: classes.taskMarker }).range(from, to));
|
|
337
|
+
} else {
|
|
338
|
+
// Replace with interactive checkbox
|
|
339
|
+
decorations.push(
|
|
340
|
+
Decoration.replace({
|
|
341
|
+
widget: new TaskCheckboxWidget(isChecked),
|
|
342
|
+
}).range(from, to)
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Render list nodes to HTML */
|
|
348
|
+
override renderToHTML(
|
|
349
|
+
node: SyntaxNode,
|
|
350
|
+
children: string,
|
|
351
|
+
ctx: { sliceDoc(from: number, to: number): string; sanitize(html: string): string }
|
|
352
|
+
): string | null {
|
|
353
|
+
switch (node.name) {
|
|
354
|
+
case "BulletList":
|
|
355
|
+
return `<ul class="${classes.lineUL} ${classes.preview}">${children}</ul>\n`;
|
|
356
|
+
|
|
357
|
+
case "OrderedList":
|
|
358
|
+
return `<ol class="${classes.lineOL} ${classes.preview}">${children}</ol>\n`;
|
|
359
|
+
|
|
360
|
+
case "ListItem":
|
|
361
|
+
return `<li>${children}</li>\n`;
|
|
362
|
+
|
|
363
|
+
case "Task":
|
|
364
|
+
return children;
|
|
365
|
+
|
|
366
|
+
case "TaskMarker": {
|
|
367
|
+
const text = ctx.sliceDoc(node.from, node.to);
|
|
368
|
+
const isChecked = text.includes("x") || text.includes("X");
|
|
369
|
+
return `<input type="checkbox" class="cm-draftly-task-checkbox" disabled ${isChecked ? "checked" : ""} />`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
case "ListMark":
|
|
373
|
+
return "";
|
|
374
|
+
|
|
375
|
+
default:
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ============================================================================
|
|
382
|
+
// Theme
|
|
383
|
+
// ============================================================================
|
|
384
|
+
|
|
385
|
+
const theme = createTheme({
|
|
386
|
+
default: {
|
|
387
|
+
// Indentation marker positioning
|
|
388
|
+
".cm-draftly-list-indent": {
|
|
389
|
+
overflow: "hidden",
|
|
390
|
+
display: "inline-block",
|
|
391
|
+
position: "absolute",
|
|
392
|
+
left: "calc(1rem * (var(--depth, 0) + 1))",
|
|
393
|
+
transform: "translateX(-100%)",
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
// List line layout (flexbox for marker alignment)
|
|
397
|
+
".cm-draftly-list-line-ul, .cm-draftly-list-line-ol": {
|
|
398
|
+
position: "relative",
|
|
399
|
+
paddingLeft: "calc(1rem * (var(--depth, 0) + 1)) !important",
|
|
400
|
+
display: "flex",
|
|
401
|
+
alignItems: "start",
|
|
402
|
+
},
|
|
403
|
+
".cm-draftly-list-line-ul > :first-child, .cm-draftly-list-line-ol > :first-child": {
|
|
404
|
+
flexShrink: 0,
|
|
405
|
+
},
|
|
406
|
+
|
|
407
|
+
// List marker sizing
|
|
408
|
+
".cm-draftly-list-line-ul .cm-draftly-list-mark-ul, .cm-draftly-list-line-ol .cm-draftly-list-mark-ol": {
|
|
409
|
+
whiteSpace: "pre",
|
|
410
|
+
position: "relative",
|
|
411
|
+
width: "1rem",
|
|
412
|
+
flexShrink: 0,
|
|
413
|
+
},
|
|
414
|
+
|
|
415
|
+
// Hide raw marker text when not active
|
|
416
|
+
".cm-draftly-list-mark-ul:not(.cm-draftly-active) > span, .cm-draftly-task-line .cm-draftly-list-mark-ol:not(.cm-draftly-active) > span":
|
|
417
|
+
{
|
|
418
|
+
visibility: "hidden",
|
|
419
|
+
display: "none",
|
|
420
|
+
},
|
|
421
|
+
|
|
422
|
+
// Styled bullet for unordered lists
|
|
423
|
+
".cm-draftly-list-line-ul .cm-draftly-list-mark-ul:not(.cm-draftly-active)::after": {
|
|
424
|
+
content: '"•"',
|
|
425
|
+
color: "var(--color-link)",
|
|
426
|
+
fontWeight: "bold",
|
|
427
|
+
pointerEvents: "none",
|
|
428
|
+
},
|
|
429
|
+
|
|
430
|
+
// Task marker styling (visible when editing)
|
|
431
|
+
".cm-draftly-task-marker": {
|
|
432
|
+
color: "var(--draftly-highlight, #a4a4a4)",
|
|
433
|
+
fontFamily: "monospace",
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
// Task checkbox container
|
|
437
|
+
".cm-draftly-task-checkbox": {
|
|
438
|
+
display: "inline-flex",
|
|
439
|
+
verticalAlign: "middle",
|
|
440
|
+
marginRight: "0.3em",
|
|
441
|
+
cursor: "pointer",
|
|
442
|
+
userSelect: "none",
|
|
443
|
+
alignItems: "center",
|
|
444
|
+
height: "1.2em",
|
|
445
|
+
},
|
|
446
|
+
|
|
447
|
+
// Task checkbox input styling
|
|
448
|
+
".cm-draftly-task-checkbox input": {
|
|
449
|
+
cursor: "pointer",
|
|
450
|
+
margin: 0,
|
|
451
|
+
width: "1.1em",
|
|
452
|
+
height: "1.1em",
|
|
453
|
+
appearance: "none",
|
|
454
|
+
border: "1px solid",
|
|
455
|
+
borderRadius: "0.25em",
|
|
456
|
+
backgroundColor: "transparent",
|
|
457
|
+
position: "relative",
|
|
458
|
+
},
|
|
459
|
+
|
|
460
|
+
// Checkmark for completed tasks
|
|
461
|
+
".cm-draftly-task-checkbox.checked input::after": {
|
|
462
|
+
content: '"✓"',
|
|
463
|
+
position: "absolute",
|
|
464
|
+
left: "1px",
|
|
465
|
+
top: "-3px",
|
|
466
|
+
},
|
|
467
|
+
|
|
468
|
+
// Preview styles (override editor-specific layout)
|
|
469
|
+
".cm-draftly-preview": {
|
|
470
|
+
display: "block",
|
|
471
|
+
paddingLeft: "1.5rem",
|
|
472
|
+
margin: "0.5rem 0",
|
|
473
|
+
},
|
|
474
|
+
".cm-draftly-preview li": {
|
|
475
|
+
display: "list-item",
|
|
476
|
+
marginBottom: "0.25rem",
|
|
477
|
+
},
|
|
478
|
+
"ul.cm-draftly-preview": {
|
|
479
|
+
listStyleType: "disc",
|
|
480
|
+
},
|
|
481
|
+
"ol.cm-draftly-preview": {
|
|
482
|
+
listStyleType: "decimal",
|
|
483
|
+
},
|
|
484
|
+
// Hide list marker for task items
|
|
485
|
+
".cm-draftly-preview li:has(.cm-draftly-task-checkbox)": {
|
|
486
|
+
listStyleType: "none",
|
|
487
|
+
},
|
|
488
|
+
".cm-draftly-preview li .cm-draftly-paragraph": {
|
|
489
|
+
padding: "0",
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
});
|