draftly 1.0.7 → 2.0.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/README.md +12 -0
- package/dist/chunk-3T55CBNZ.cjs +33 -0
- package/dist/chunk-3T55CBNZ.cjs.map +1 -0
- package/dist/chunk-5MC4T7JH.cjs +58 -0
- package/dist/chunk-5MC4T7JH.cjs.map +1 -0
- package/dist/{chunk-72ZYRGRT.cjs → chunk-BWJLMREN.cjs} +11 -9
- package/dist/chunk-BWJLMREN.cjs.map +1 -0
- package/dist/{chunk-KBQDZ5IW.cjs → chunk-CLW73JRX.cjs} +100 -75
- package/dist/chunk-CLW73JRX.cjs.map +1 -0
- package/dist/{chunk-DFQYXFOP.js → chunk-EEHILRG5.js} +26 -3
- package/dist/chunk-EEHILRG5.js.map +1 -0
- package/dist/{chunk-HPSMS2WB.js → chunk-I563H35S.js} +101 -75
- package/dist/chunk-I563H35S.js.map +1 -0
- package/dist/chunk-IAXF4SJL.js +55 -0
- package/dist/chunk-IAXF4SJL.js.map +1 -0
- package/dist/chunk-JF3WXXMJ.js +31 -0
- package/dist/chunk-JF3WXXMJ.js.map +1 -0
- package/dist/{chunk-N3WL3XPB.js → chunk-L2XSK57Y.js} +1761 -478
- package/dist/chunk-L2XSK57Y.js.map +1 -0
- package/dist/{chunk-KDEDLC3D.cjs → chunk-TBVZEK2H.cjs} +27 -2
- package/dist/chunk-TBVZEK2H.cjs.map +1 -0
- package/dist/{chunk-2B3A3VSQ.cjs → chunk-W5ALMXG2.cjs} +1808 -504
- package/dist/chunk-W5ALMXG2.cjs.map +1 -0
- package/dist/{chunk-CG4M4TC7.js → chunk-ZUI3GI3W.js} +7 -5
- package/dist/chunk-ZUI3GI3W.js.map +1 -0
- package/dist/{draftly-BLnx3uGX.d.cts → draftly-BBL-AdOl.d.cts} +5 -1
- package/dist/{draftly-BLnx3uGX.d.ts → draftly-BBL-AdOl.d.ts} +5 -1
- package/dist/editor/index.cjs +22 -14
- package/dist/editor/index.d.cts +2 -1
- package/dist/editor/index.d.ts +2 -1
- package/dist/editor/index.js +2 -2
- package/dist/index.cjs +65 -39
- package/dist/index.d.cts +6 -3
- package/dist/index.d.ts +6 -3
- package/dist/index.js +6 -4
- package/dist/lib/index.cjs +12 -0
- package/dist/lib/index.cjs.map +1 -0
- package/dist/lib/index.d.cts +16 -0
- package/dist/lib/index.d.ts +16 -0
- package/dist/lib/index.js +3 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/plugins/index.cjs +27 -17
- package/dist/plugins/index.d.cts +144 -9
- package/dist/plugins/index.d.ts +144 -9
- package/dist/plugins/index.js +5 -3
- package/dist/preview/index.cjs +16 -11
- package/dist/preview/index.d.cts +19 -4
- package/dist/preview/index.d.ts +19 -4
- package/dist/preview/index.js +3 -2
- package/package.json +8 -1
- package/src/editor/draftly.ts +1 -0
- package/src/editor/plugin.ts +5 -1
- package/src/editor/theme.ts +1 -0
- package/src/editor/utils.ts +31 -0
- package/src/index.ts +5 -4
- package/src/lib/index.ts +2 -0
- package/src/lib/input-handler.ts +45 -0
- package/src/plugins/code-plugin.theme.ts +426 -0
- package/src/plugins/code-plugin.ts +810 -561
- package/src/plugins/emoji-plugin.ts +140 -0
- package/src/plugins/index.ts +63 -57
- package/src/plugins/inline-plugin.ts +305 -291
- package/src/plugins/math-plugin.ts +12 -0
- package/src/plugins/table-plugin.ts +900 -0
- package/src/preview/context.ts +4 -1
- package/src/preview/css-generator.ts +14 -1
- package/src/preview/index.ts +9 -1
- package/src/preview/preview.ts +2 -1
- package/src/preview/renderer.ts +21 -20
- package/src/preview/syntax-theme.ts +110 -0
- package/src/preview/types.ts +14 -0
- package/dist/chunk-2B3A3VSQ.cjs.map +0 -1
- package/dist/chunk-72ZYRGRT.cjs.map +0 -1
- package/dist/chunk-CG4M4TC7.js.map +0 -1
- package/dist/chunk-DFQYXFOP.js.map +0 -1
- package/dist/chunk-HPSMS2WB.js.map +0 -1
- package/dist/chunk-KBQDZ5IW.cjs.map +0 -1
- package/dist/chunk-KDEDLC3D.cjs.map +0 -1
- package/dist/chunk-N3WL3XPB.js.map +0 -1
|
@@ -0,0 +1,900 @@
|
|
|
1
|
+
import { Decoration, EditorView, KeyBinding, WidgetType } from "@codemirror/view";
|
|
2
|
+
import { syntaxTree } from "@codemirror/language";
|
|
3
|
+
import { DecorationContext, DecorationPlugin, PluginContext } from "../editor/plugin";
|
|
4
|
+
import { createTheme, ThemeEnum } from "../editor";
|
|
5
|
+
import { SyntaxNode } from "@lezer/common";
|
|
6
|
+
import { DraftlyConfig } from "../editor/draftly";
|
|
7
|
+
import { PreviewRenderer } from "../preview/renderer";
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Types
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
/** Column alignment parsed from the delimiter row */
|
|
14
|
+
type Alignment = "left" | "center" | "right";
|
|
15
|
+
|
|
16
|
+
/** Parsed table structure */
|
|
17
|
+
interface ParsedTable {
|
|
18
|
+
/** Header row cells */
|
|
19
|
+
headers: string[];
|
|
20
|
+
/** Column alignments */
|
|
21
|
+
alignments: Alignment[];
|
|
22
|
+
/** Data rows, each an array of cell strings */
|
|
23
|
+
rows: string[][];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type PreviewContextLike = {
|
|
27
|
+
sliceDoc(from: number, to: number): string;
|
|
28
|
+
sanitize(html: string): string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// Utilities
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parse alignment from a delimiter cell (e.g., `:---:`, `---:`, `:---`, `---`)
|
|
37
|
+
*/
|
|
38
|
+
function parseAlignment(cell: string): Alignment {
|
|
39
|
+
const trimmed = cell.trim();
|
|
40
|
+
const left = trimmed.startsWith(":");
|
|
41
|
+
const right = trimmed.endsWith(":");
|
|
42
|
+
if (left && right) return "center";
|
|
43
|
+
if (right) return "right";
|
|
44
|
+
return "left";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Parse a markdown table string into structured data
|
|
49
|
+
*/
|
|
50
|
+
function parseTableMarkdown(markdown: string): ParsedTable | null {
|
|
51
|
+
const lines = markdown.split("\n").filter((l) => l.trim().length > 0);
|
|
52
|
+
if (lines.length < 2) return null;
|
|
53
|
+
|
|
54
|
+
const parseCells = (line: string): string[] => {
|
|
55
|
+
// Remove leading/trailing pipes, then split by pipe
|
|
56
|
+
let trimmed = line.trim();
|
|
57
|
+
if (trimmed.startsWith("|")) trimmed = trimmed.slice(1);
|
|
58
|
+
if (trimmed.endsWith("|")) trimmed = trimmed.slice(0, -1);
|
|
59
|
+
return trimmed.split("|").map((c) => c.trim());
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const headers = parseCells(lines[0]!);
|
|
63
|
+
const delimiterCells = parseCells(lines[1]!);
|
|
64
|
+
|
|
65
|
+
// Validate delimiter row (must contain only -, :, and spaces)
|
|
66
|
+
const isDelimiter = delimiterCells.every((c) => /^:?-+:?$/.test(c.trim()));
|
|
67
|
+
if (!isDelimiter) return null;
|
|
68
|
+
|
|
69
|
+
const alignments = delimiterCells.map(parseAlignment);
|
|
70
|
+
|
|
71
|
+
const rows: string[][] = [];
|
|
72
|
+
for (let i = 2; i < lines.length; i++) {
|
|
73
|
+
rows.push(parseCells(lines[i]!));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { headers, alignments, rows };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Check if a row is completely empty (all cells are empty/whitespace)
|
|
81
|
+
*/
|
|
82
|
+
function isRowEmpty(rowText: string): boolean {
|
|
83
|
+
const trimmed = rowText.trim();
|
|
84
|
+
if (!trimmed.startsWith("|")) return false;
|
|
85
|
+
let inner = trimmed;
|
|
86
|
+
if (inner.startsWith("|")) inner = inner.slice(1);
|
|
87
|
+
if (inner.endsWith("|")) inner = inner.slice(0, -1);
|
|
88
|
+
return inner.split("|").every((cell) => cell.trim() === "");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function renderCellWithPreviewRenderer(text: string, config?: DraftlyConfig): Promise<string> {
|
|
92
|
+
if (!text.trim()) {
|
|
93
|
+
return " ";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const renderer = new PreviewRenderer(
|
|
97
|
+
text,
|
|
98
|
+
config?.plugins || [],
|
|
99
|
+
config?.markdown || [],
|
|
100
|
+
config?.theme || ThemeEnum.AUTO,
|
|
101
|
+
true
|
|
102
|
+
);
|
|
103
|
+
const html = await renderer.render();
|
|
104
|
+
|
|
105
|
+
// If wrapped in a single paragraph, unwrap for table cell display
|
|
106
|
+
const paragraphMatch = html.match(/^\s*<p>([\s\S]*)<\/p>\s*$/i);
|
|
107
|
+
if (paragraphMatch && paragraphMatch[1] !== undefined) {
|
|
108
|
+
return paragraphMatch[1];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return html;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function getColumnAlignment(alignments: Alignment[], index: number): Alignment {
|
|
115
|
+
return alignments[index] || "left";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getColumnCount(headers: string[], row: string[]): number {
|
|
119
|
+
return Math.max(headers.length, row.length);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function renderTableToHtml(parsed: ParsedTable, config?: DraftlyConfig): Promise<string> {
|
|
123
|
+
const { headers, alignments, rows } = parsed;
|
|
124
|
+
let html = '<div class="cm-draftly-table-widget">';
|
|
125
|
+
html += '<table class="cm-draftly-table">';
|
|
126
|
+
|
|
127
|
+
html += "<thead><tr>";
|
|
128
|
+
for (let i = 0; i < headers.length; i++) {
|
|
129
|
+
const cell = headers[i] || "";
|
|
130
|
+
const align = getColumnAlignment(alignments, i);
|
|
131
|
+
const rendered = await renderCellWithPreviewRenderer(cell, config);
|
|
132
|
+
html += `<th style="text-align: ${align}">${rendered}</th>`;
|
|
133
|
+
}
|
|
134
|
+
html += "</tr></thead>";
|
|
135
|
+
|
|
136
|
+
html += "<tbody>";
|
|
137
|
+
for (const row of rows) {
|
|
138
|
+
html += "<tr>";
|
|
139
|
+
const colCount = getColumnCount(headers, row);
|
|
140
|
+
for (let i = 0; i < colCount; i++) {
|
|
141
|
+
const align = getColumnAlignment(alignments, i);
|
|
142
|
+
const cell = row[i] || "";
|
|
143
|
+
const rendered = await renderCellWithPreviewRenderer(cell, config);
|
|
144
|
+
html += `<td style="text-align: ${align}">${rendered}</td>`;
|
|
145
|
+
}
|
|
146
|
+
html += "</tr>";
|
|
147
|
+
}
|
|
148
|
+
html += "</tbody>";
|
|
149
|
+
|
|
150
|
+
html += "</table></div>";
|
|
151
|
+
return html;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ============================================================================
|
|
155
|
+
// Widget
|
|
156
|
+
// ============================================================================
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Widget to render a markdown table as a styled HTML table.
|
|
160
|
+
* Shows rounded borders, alternate row colors, cell borders, and alignments.
|
|
161
|
+
*/
|
|
162
|
+
class TableWidget extends WidgetType {
|
|
163
|
+
constructor(
|
|
164
|
+
readonly tableMarkdown: string,
|
|
165
|
+
readonly from: number,
|
|
166
|
+
readonly to: number,
|
|
167
|
+
readonly config?: DraftlyConfig
|
|
168
|
+
) {
|
|
169
|
+
super();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
override eq(other: TableWidget): boolean {
|
|
173
|
+
return (
|
|
174
|
+
other.tableMarkdown === this.tableMarkdown &&
|
|
175
|
+
other.from === this.from &&
|
|
176
|
+
other.to === this.to &&
|
|
177
|
+
other.config === this.config
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
toDOM(view: EditorView): HTMLElement {
|
|
182
|
+
const wrapper = document.createElement("div");
|
|
183
|
+
wrapper.className = "cm-draftly-table-widget";
|
|
184
|
+
|
|
185
|
+
const parsed = parseTableMarkdown(this.tableMarkdown);
|
|
186
|
+
if (!parsed) {
|
|
187
|
+
wrapper.textContent = "[Invalid table]";
|
|
188
|
+
return wrapper;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const { headers, alignments, rows } = parsed;
|
|
192
|
+
|
|
193
|
+
// Build the table
|
|
194
|
+
const table = document.createElement("table");
|
|
195
|
+
table.className = "cm-draftly-table";
|
|
196
|
+
|
|
197
|
+
// Thead
|
|
198
|
+
const thead = document.createElement("thead");
|
|
199
|
+
const headerRow = document.createElement("tr");
|
|
200
|
+
headers.forEach((h, i) => {
|
|
201
|
+
const th = document.createElement("th");
|
|
202
|
+
th.innerHTML = " ";
|
|
203
|
+
this.renderCellAsync(h, th);
|
|
204
|
+
const align = alignments[i];
|
|
205
|
+
if (align) th.style.textAlign = align;
|
|
206
|
+
headerRow.appendChild(th);
|
|
207
|
+
});
|
|
208
|
+
thead.appendChild(headerRow);
|
|
209
|
+
table.appendChild(thead);
|
|
210
|
+
|
|
211
|
+
// Tbody
|
|
212
|
+
const tbody = document.createElement("tbody");
|
|
213
|
+
rows.forEach((row) => {
|
|
214
|
+
const tr = document.createElement("tr");
|
|
215
|
+
// Ensure we render enough cells for the column count
|
|
216
|
+
const colCount = getColumnCount(headers, row);
|
|
217
|
+
for (let i = 0; i < colCount; i++) {
|
|
218
|
+
const td = document.createElement("td");
|
|
219
|
+
td.innerHTML = " ";
|
|
220
|
+
this.renderCellAsync(row[i] || "", td);
|
|
221
|
+
const align = getColumnAlignment(alignments, i);
|
|
222
|
+
if (align) td.style.textAlign = align;
|
|
223
|
+
tr.appendChild(td);
|
|
224
|
+
}
|
|
225
|
+
tbody.appendChild(tr);
|
|
226
|
+
});
|
|
227
|
+
table.appendChild(tbody);
|
|
228
|
+
|
|
229
|
+
wrapper.appendChild(table);
|
|
230
|
+
|
|
231
|
+
// Click handler — set cursor inside table to reveal raw markdown
|
|
232
|
+
wrapper.addEventListener("click", (e) => {
|
|
233
|
+
e.preventDefault();
|
|
234
|
+
e.stopPropagation();
|
|
235
|
+
view.dispatch({
|
|
236
|
+
selection: { anchor: this.from },
|
|
237
|
+
scrollIntoView: true,
|
|
238
|
+
});
|
|
239
|
+
view.focus();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
return wrapper;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Render cell content asynchronously using PreviewRenderer
|
|
247
|
+
*/
|
|
248
|
+
private async renderCellAsync(text: string, element: HTMLElement) {
|
|
249
|
+
if (!text.trim()) {
|
|
250
|
+
element.innerHTML = " ";
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
element.innerHTML = await renderCellWithPreviewRenderer(text, this.config);
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.error("Failed to render table cell:", error);
|
|
258
|
+
element.textContent = text;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
override ignoreEvent(event: Event): boolean {
|
|
263
|
+
return event.type !== "click";
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ============================================================================
|
|
268
|
+
// Decorations
|
|
269
|
+
// ============================================================================
|
|
270
|
+
|
|
271
|
+
const tableMarkDecorations = {
|
|
272
|
+
"table-line": Decoration.line({ class: "cm-draftly-table-line" }),
|
|
273
|
+
"table-line-start": Decoration.line({ class: "cm-draftly-table-line-start" }),
|
|
274
|
+
"table-line-end": Decoration.line({ class: "cm-draftly-table-line-end" }),
|
|
275
|
+
"table-delimiter": Decoration.line({ class: "cm-draftly-table-delimiter-line" }),
|
|
276
|
+
"table-rendered": Decoration.line({ class: "cm-draftly-table-rendered" }),
|
|
277
|
+
// "table-hidden": Decoration.mark({ class: "cm-draftly-table-hidden" }),
|
|
278
|
+
"table-hidden": Decoration.replace({}),
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// ============================================================================
|
|
282
|
+
// Plugin
|
|
283
|
+
// ============================================================================
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* TablePlugin — Renders GFM markdown tables as styled widgets.
|
|
287
|
+
*
|
|
288
|
+
* Features:
|
|
289
|
+
* - Rendered table widget with rounded borders, alternate row colors, cell borders
|
|
290
|
+
* - Alignment support (`:---:`, `----:`, `:---`)
|
|
291
|
+
* - Monospace raw markdown when cursor is inside the table
|
|
292
|
+
* - Keyboard shortcuts for table creation, adding rows/columns
|
|
293
|
+
* - Enter in last row/last cell: creates row, again removes it
|
|
294
|
+
* - Auto-formats table markdown to align pipes
|
|
295
|
+
*/
|
|
296
|
+
export class TablePlugin extends DecorationPlugin {
|
|
297
|
+
readonly name = "table";
|
|
298
|
+
readonly version = "1.0.0";
|
|
299
|
+
override decorationPriority = 20;
|
|
300
|
+
override readonly requiredNodes = ["Table", "TableHeader", "TableDelimiter", "TableRow", "TableCell"] as const;
|
|
301
|
+
|
|
302
|
+
/** Configuration stored from onRegister */
|
|
303
|
+
private draftlyConfig: DraftlyConfig | undefined;
|
|
304
|
+
|
|
305
|
+
override onRegister(context: PluginContext): void {
|
|
306
|
+
super.onRegister(context);
|
|
307
|
+
this.draftlyConfig = context.config;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
override get theme() {
|
|
311
|
+
return theme;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ============================================
|
|
315
|
+
// Keymaps
|
|
316
|
+
// ============================================
|
|
317
|
+
|
|
318
|
+
override getKeymap(): KeyBinding[] {
|
|
319
|
+
return [
|
|
320
|
+
{
|
|
321
|
+
key: "Mod-Shift-t",
|
|
322
|
+
run: (view) => this.insertTable(view),
|
|
323
|
+
preventDefault: true,
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
key: "Mod-Enter",
|
|
327
|
+
run: (view) => this.addRow(view),
|
|
328
|
+
preventDefault: true,
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
key: "Mod-Shift-Enter",
|
|
332
|
+
run: (view) => this.addColumn(view),
|
|
333
|
+
preventDefault: true,
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
key: "Enter",
|
|
337
|
+
run: (view) => this.handleEnter(view),
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
key: "Tab",
|
|
341
|
+
run: (view) => this.handleTab(view, false),
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
key: "Shift-Tab",
|
|
345
|
+
run: (view) => this.handleTab(view, true),
|
|
346
|
+
},
|
|
347
|
+
];
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ============================================
|
|
351
|
+
// Decorations
|
|
352
|
+
// ============================================
|
|
353
|
+
|
|
354
|
+
buildDecorations(ctx: DecorationContext): void {
|
|
355
|
+
const { view, decorations } = ctx;
|
|
356
|
+
const tree = syntaxTree(view.state);
|
|
357
|
+
|
|
358
|
+
tree.iterate({
|
|
359
|
+
enter: (node) => {
|
|
360
|
+
if (node.name !== "Table") return;
|
|
361
|
+
|
|
362
|
+
const { from, to } = node;
|
|
363
|
+
const nodeLineStart = view.state.doc.lineAt(from);
|
|
364
|
+
const nodeLineEnd = view.state.doc.lineAt(to);
|
|
365
|
+
const cursorInRange = ctx.selectionOverlapsRange(nodeLineStart.from, nodeLineEnd.to);
|
|
366
|
+
|
|
367
|
+
if (cursorInRange) {
|
|
368
|
+
// Cursor inside: show raw markdown with monospace styling
|
|
369
|
+
// Add line decorations for every line in the table
|
|
370
|
+
for (let i = nodeLineStart.number; i <= nodeLineEnd.number; i++) {
|
|
371
|
+
const line = view.state.doc.line(i);
|
|
372
|
+
decorations.push(tableMarkDecorations["table-line"].range(line.from));
|
|
373
|
+
|
|
374
|
+
if (i === nodeLineStart.number) {
|
|
375
|
+
decorations.push(tableMarkDecorations["table-line-start"].range(line.from));
|
|
376
|
+
}
|
|
377
|
+
if (i === nodeLineEnd.number) {
|
|
378
|
+
decorations.push(tableMarkDecorations["table-line-end"].range(line.from));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Check if this is the delimiter line (line 2 of the table)
|
|
382
|
+
if (i === nodeLineStart.number + 1) {
|
|
383
|
+
decorations.push(tableMarkDecorations["table-delimiter"].range(line.from));
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
} else {
|
|
387
|
+
// Cursor outside: hide raw text and show rendered widget
|
|
388
|
+
const tableContent = view.state.sliceDoc(from, to);
|
|
389
|
+
|
|
390
|
+
// Add line decorations to hide all lines
|
|
391
|
+
for (let i = nodeLineStart.number; i <= nodeLineEnd.number; i++) {
|
|
392
|
+
const line = view.state.doc.line(i);
|
|
393
|
+
decorations.push(tableMarkDecorations["table-rendered"].range(line.from));
|
|
394
|
+
|
|
395
|
+
// Hide the raw text content
|
|
396
|
+
decorations.push(tableMarkDecorations["table-hidden"].range(line.from, line.to));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Add the rendered table widget at the end
|
|
400
|
+
decorations.push(
|
|
401
|
+
Decoration.widget({
|
|
402
|
+
widget: new TableWidget(tableContent, from, to, this.draftlyConfig),
|
|
403
|
+
side: 1,
|
|
404
|
+
block: false,
|
|
405
|
+
}).range(to)
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ============================================
|
|
413
|
+
// Keymap Handlers
|
|
414
|
+
// ============================================
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Insert a new 3×3 table at cursor position
|
|
418
|
+
*/
|
|
419
|
+
private insertTable(view: EditorView): boolean {
|
|
420
|
+
const { state } = view;
|
|
421
|
+
const cursor = state.selection.main.head;
|
|
422
|
+
const line = state.doc.lineAt(cursor);
|
|
423
|
+
|
|
424
|
+
// Insert at the beginning of the next line if current line has content
|
|
425
|
+
const insertPos = line.text.trim() ? line.to : line.from;
|
|
426
|
+
|
|
427
|
+
const template = [
|
|
428
|
+
"| Header 1 | Header 2 | Header 3 |",
|
|
429
|
+
"| -------- | -------- | -------- |",
|
|
430
|
+
"| | | |",
|
|
431
|
+
].join("\n");
|
|
432
|
+
|
|
433
|
+
const prefix = line.text.trim() ? "\n" : "";
|
|
434
|
+
const suffix = "\n";
|
|
435
|
+
|
|
436
|
+
view.dispatch({
|
|
437
|
+
changes: {
|
|
438
|
+
from: insertPos,
|
|
439
|
+
insert: prefix + template + suffix,
|
|
440
|
+
},
|
|
441
|
+
selection: {
|
|
442
|
+
anchor: insertPos + prefix.length + 2, // Position cursor in first header cell
|
|
443
|
+
},
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Add a new row below the current row (Mod-Enter)
|
|
451
|
+
*/
|
|
452
|
+
private addRow(view: EditorView): boolean {
|
|
453
|
+
const tableInfo = this.getTableAtCursor(view);
|
|
454
|
+
if (!tableInfo) return false;
|
|
455
|
+
|
|
456
|
+
const { state } = view;
|
|
457
|
+
const cursor = state.selection.main.head;
|
|
458
|
+
const currentLine = state.doc.lineAt(cursor);
|
|
459
|
+
|
|
460
|
+
// Parse the table to know the column count
|
|
461
|
+
const parsed = parseTableMarkdown(state.sliceDoc(tableInfo.from, tableInfo.to));
|
|
462
|
+
if (!parsed) return false;
|
|
463
|
+
|
|
464
|
+
const colCount = parsed.headers.length;
|
|
465
|
+
const emptyRow = "| " + Array.from({ length: colCount }, () => " ").join(" | ") + " |";
|
|
466
|
+
|
|
467
|
+
// Insert after the current line
|
|
468
|
+
view.dispatch({
|
|
469
|
+
changes: {
|
|
470
|
+
from: currentLine.to,
|
|
471
|
+
insert: "\n" + emptyRow,
|
|
472
|
+
},
|
|
473
|
+
selection: {
|
|
474
|
+
anchor: currentLine.to + 3, // Position in first cell of new row
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Add a new column after the current column (Mod-Shift-Enter)
|
|
483
|
+
*/
|
|
484
|
+
private addColumn(view: EditorView): boolean {
|
|
485
|
+
const tableInfo = this.getTableAtCursor(view);
|
|
486
|
+
if (!tableInfo) return false;
|
|
487
|
+
|
|
488
|
+
const { state } = view;
|
|
489
|
+
const cursor = state.selection.main.head;
|
|
490
|
+
|
|
491
|
+
// Find which column the cursor is in
|
|
492
|
+
const currentLine = state.doc.lineAt(cursor);
|
|
493
|
+
const lineText = currentLine.text;
|
|
494
|
+
const cursorInLine = cursor - currentLine.from;
|
|
495
|
+
|
|
496
|
+
// Count pipes before cursor to find column index
|
|
497
|
+
let colIndex = -1;
|
|
498
|
+
for (let i = 0; i < cursorInLine; i++) {
|
|
499
|
+
if (lineText[i] === "|") colIndex++;
|
|
500
|
+
}
|
|
501
|
+
colIndex = Math.max(0, colIndex);
|
|
502
|
+
|
|
503
|
+
// Get all lines of the table
|
|
504
|
+
const tableText = state.sliceDoc(tableInfo.from, tableInfo.to);
|
|
505
|
+
const lines = tableText.split("\n");
|
|
506
|
+
|
|
507
|
+
// Insert a new column after colIndex in each line
|
|
508
|
+
const newLines = lines.map((line, lineIdx) => {
|
|
509
|
+
const cells = this.splitLineToCells(line);
|
|
510
|
+
const insertAfter = Math.min(colIndex, cells.length - 1);
|
|
511
|
+
|
|
512
|
+
if (lineIdx === 1) {
|
|
513
|
+
// Delimiter row
|
|
514
|
+
cells.splice(insertAfter + 1, 0, " -------- ");
|
|
515
|
+
} else {
|
|
516
|
+
cells.splice(insertAfter + 1, 0, " ");
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return "|" + cells.join("|") + "|";
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
view.dispatch({
|
|
523
|
+
changes: {
|
|
524
|
+
from: tableInfo.from,
|
|
525
|
+
to: tableInfo.to,
|
|
526
|
+
insert: newLines.join("\n"),
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Handle Enter key inside a table.
|
|
535
|
+
* - Last cell of last row: create a new row
|
|
536
|
+
* - Empty last row: remove it and move cursor after table
|
|
537
|
+
*/
|
|
538
|
+
private handleEnter(view: EditorView): boolean {
|
|
539
|
+
const tableInfo = this.getTableAtCursor(view);
|
|
540
|
+
if (!tableInfo) return false;
|
|
541
|
+
|
|
542
|
+
const { state } = view;
|
|
543
|
+
const cursor = state.selection.main.head;
|
|
544
|
+
const cursorLine = state.doc.lineAt(cursor);
|
|
545
|
+
const tableEndLine = state.doc.lineAt(tableInfo.to);
|
|
546
|
+
|
|
547
|
+
// Check if cursor is on the last line of the table
|
|
548
|
+
if (cursorLine.number !== tableEndLine.number) return false;
|
|
549
|
+
|
|
550
|
+
// Check if cursor is in the last cell (after the second-to-last pipe)
|
|
551
|
+
const lineText = cursorLine.text;
|
|
552
|
+
const cursorOffset = cursor - cursorLine.from;
|
|
553
|
+
const pipes: number[] = [];
|
|
554
|
+
for (let i = 0; i < lineText.length; i++) {
|
|
555
|
+
if (lineText[i] === "|") pipes.push(i);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Cursor needs to be after the second-to-last pipe (in the last cell)
|
|
559
|
+
if (pipes.length < 2) return false;
|
|
560
|
+
const lastCellStart = pipes[pipes.length - 2]!;
|
|
561
|
+
if (cursorOffset < lastCellStart) return false;
|
|
562
|
+
|
|
563
|
+
// If this row is empty, remove it and move cursor after the table
|
|
564
|
+
if (isRowEmpty(lineText)) {
|
|
565
|
+
// Remove this row (including the preceding newline)
|
|
566
|
+
const removeFrom = cursorLine.from - 1; // Include the newline before
|
|
567
|
+
const removeTo = cursorLine.to;
|
|
568
|
+
|
|
569
|
+
view.dispatch({
|
|
570
|
+
changes: { from: Math.max(0, removeFrom), to: removeTo },
|
|
571
|
+
selection: {
|
|
572
|
+
anchor: Math.min(Math.max(0, removeFrom) + 1, view.state.doc.length),
|
|
573
|
+
},
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
return true;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Otherwise, create a new empty row
|
|
580
|
+
const parsed = parseTableMarkdown(state.sliceDoc(tableInfo.from, tableInfo.to));
|
|
581
|
+
if (!parsed) return false;
|
|
582
|
+
|
|
583
|
+
const colCount = parsed.headers.length;
|
|
584
|
+
const emptyRow = "| " + Array.from({ length: colCount }, () => " ").join(" | ") + " |";
|
|
585
|
+
|
|
586
|
+
view.dispatch({
|
|
587
|
+
changes: {
|
|
588
|
+
from: cursorLine.to,
|
|
589
|
+
insert: "\n" + emptyRow,
|
|
590
|
+
},
|
|
591
|
+
selection: {
|
|
592
|
+
anchor: cursorLine.to + 3, // Position in first cell of new row
|
|
593
|
+
},
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
return true;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Handle Tab key inside a table — move to next/previous cell
|
|
601
|
+
*/
|
|
602
|
+
private handleTab(view: EditorView, backwards: boolean): boolean {
|
|
603
|
+
const tableInfo = this.getTableAtCursor(view);
|
|
604
|
+
if (!tableInfo) return false;
|
|
605
|
+
|
|
606
|
+
const { state } = view;
|
|
607
|
+
const cursor = state.selection.main.head;
|
|
608
|
+
const tableText = state.sliceDoc(tableInfo.from, tableInfo.to);
|
|
609
|
+
const lines = tableText.split("\n");
|
|
610
|
+
|
|
611
|
+
// Collect all cell positions (skip delimiter row)
|
|
612
|
+
const cellPositions: { lineFrom: number; start: number; end: number }[] = [];
|
|
613
|
+
for (let li = 0; li < lines.length; li++) {
|
|
614
|
+
if (li === 1) continue; // Skip delimiter row
|
|
615
|
+
const line = lines[li]!;
|
|
616
|
+
const lineFrom = tableInfo.from + lines.slice(0, li).reduce((sum, l) => sum + l.length + 1, 0);
|
|
617
|
+
|
|
618
|
+
const pipes: number[] = [];
|
|
619
|
+
for (let i = 0; i < line.length; i++) {
|
|
620
|
+
if (line[i] === "|") pipes.push(i);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
for (let p = 0; p < pipes.length - 1; p++) {
|
|
624
|
+
const cellStart = pipes[p]! + 1;
|
|
625
|
+
const cellEnd = pipes[p + 1]!;
|
|
626
|
+
cellPositions.push({
|
|
627
|
+
lineFrom,
|
|
628
|
+
start: cellStart,
|
|
629
|
+
end: cellEnd,
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Find which cell the cursor is currently in
|
|
635
|
+
let currentCellIdx = -1;
|
|
636
|
+
for (let i = 0; i < cellPositions.length; i++) {
|
|
637
|
+
const cell = cellPositions[i]!;
|
|
638
|
+
const absStart = cell.lineFrom + cell.start;
|
|
639
|
+
const absEnd = cell.lineFrom + cell.end;
|
|
640
|
+
if (cursor >= absStart && cursor <= absEnd) {
|
|
641
|
+
currentCellIdx = i;
|
|
642
|
+
break;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (currentCellIdx === -1) return false;
|
|
647
|
+
|
|
648
|
+
// Move to next/previous cell
|
|
649
|
+
const nextIdx = backwards ? currentCellIdx - 1 : currentCellIdx + 1;
|
|
650
|
+
if (nextIdx < 0 || nextIdx >= cellPositions.length) return false;
|
|
651
|
+
|
|
652
|
+
const nextCell = cellPositions[nextIdx]!;
|
|
653
|
+
const cellText = state.sliceDoc(nextCell.lineFrom + nextCell.start, nextCell.lineFrom + nextCell.end);
|
|
654
|
+
const trimStart = cellText.length - cellText.trimStart().length;
|
|
655
|
+
const trimEnd = cellText.length - cellText.trimEnd().length;
|
|
656
|
+
|
|
657
|
+
const selectFrom = nextCell.lineFrom + nextCell.start + (trimStart > 0 ? 1 : 0);
|
|
658
|
+
const selectTo = nextCell.lineFrom + nextCell.end - (trimEnd > 0 ? 1 : 0);
|
|
659
|
+
|
|
660
|
+
view.dispatch({
|
|
661
|
+
selection: {
|
|
662
|
+
anchor: selectFrom,
|
|
663
|
+
head: selectTo,
|
|
664
|
+
},
|
|
665
|
+
scrollIntoView: true,
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
return true;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// ============================================
|
|
672
|
+
// Helpers
|
|
673
|
+
// ============================================
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Find the Table node at the cursor position
|
|
677
|
+
*/
|
|
678
|
+
private getTableAtCursor(view: EditorView): { from: number; to: number } | null {
|
|
679
|
+
const tree = syntaxTree(view.state);
|
|
680
|
+
const cursor = view.state.selection.main.head;
|
|
681
|
+
|
|
682
|
+
let result: { from: number; to: number } | null = null;
|
|
683
|
+
tree.iterate({
|
|
684
|
+
enter: (node) => {
|
|
685
|
+
if (node.name === "Table" && cursor >= node.from && cursor <= node.to) {
|
|
686
|
+
result = { from: node.from, to: node.to };
|
|
687
|
+
}
|
|
688
|
+
},
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
return result;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Split a table line into cells (keeping the whitespace around content)
|
|
696
|
+
*/
|
|
697
|
+
private splitLineToCells(line: string): string[] {
|
|
698
|
+
let trimmed = line.trim();
|
|
699
|
+
if (trimmed.startsWith("|")) trimmed = trimmed.slice(1);
|
|
700
|
+
if (trimmed.endsWith("|")) trimmed = trimmed.slice(0, -1);
|
|
701
|
+
return trimmed.split("|");
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// ============================================
|
|
705
|
+
// Preview Rendering
|
|
706
|
+
// ============================================
|
|
707
|
+
|
|
708
|
+
override async renderToHTML(
|
|
709
|
+
node: SyntaxNode,
|
|
710
|
+
_children: string,
|
|
711
|
+
_ctx: PreviewContextLike
|
|
712
|
+
): Promise<string | null> {
|
|
713
|
+
if (node.name === "Table") {
|
|
714
|
+
const content = _ctx.sliceDoc(node.from, node.to);
|
|
715
|
+
const parsed = parseTableMarkdown(content);
|
|
716
|
+
|
|
717
|
+
if (!parsed) return null;
|
|
718
|
+
|
|
719
|
+
return await renderTableToHtml(parsed, this.draftlyConfig);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Sub-nodes are handled by the Table renderer
|
|
723
|
+
if (
|
|
724
|
+
node.name === "TableHeader" ||
|
|
725
|
+
node.name === "TableDelimiter" ||
|
|
726
|
+
node.name === "TableRow" ||
|
|
727
|
+
node.name === "TableCell"
|
|
728
|
+
) {
|
|
729
|
+
return "";
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// ============================================================================
|
|
737
|
+
// Theme
|
|
738
|
+
// ============================================================================
|
|
739
|
+
|
|
740
|
+
const theme = createTheme({
|
|
741
|
+
default: {
|
|
742
|
+
// Raw table lines — monospace when cursor is inside
|
|
743
|
+
".cm-draftly-table-line": {
|
|
744
|
+
"--radius": "0.375rem",
|
|
745
|
+
fontFamily: "var(--font-jetbrains-mono, monospace)",
|
|
746
|
+
fontSize: "0.9rem",
|
|
747
|
+
backgroundColor: "rgba(0, 0, 0, 0.02)",
|
|
748
|
+
padding: "0 0.75rem !important",
|
|
749
|
+
lineHeight: "1.6",
|
|
750
|
+
borderLeft: "1px solid var(--color-border, #e2e8f0)",
|
|
751
|
+
borderRight: "1px solid var(--color-border, #e2e8f0)",
|
|
752
|
+
},
|
|
753
|
+
|
|
754
|
+
".cm-draftly-table-line-start": {
|
|
755
|
+
borderTopLeftRadius: "var(--radius)",
|
|
756
|
+
borderTopRightRadius: "var(--radius)",
|
|
757
|
+
borderTop: "1px solid var(--color-border, #e2e8f0)",
|
|
758
|
+
},
|
|
759
|
+
|
|
760
|
+
".cm-draftly-table-line-end": {
|
|
761
|
+
borderBottomLeftRadius: "var(--radius)",
|
|
762
|
+
borderBottomRightRadius: "var(--radius)",
|
|
763
|
+
borderBottom: "1px solid var(--color-border, #e2e8f0)",
|
|
764
|
+
},
|
|
765
|
+
|
|
766
|
+
".cm-draftly-table-delimiter-line": {
|
|
767
|
+
opacity: "0.5",
|
|
768
|
+
},
|
|
769
|
+
|
|
770
|
+
// Hidden table text (when cursor is not in range)
|
|
771
|
+
".cm-draftly-table-hidden": {
|
|
772
|
+
display: "none",
|
|
773
|
+
},
|
|
774
|
+
|
|
775
|
+
// Line decoration for rendered state — hide line breaks
|
|
776
|
+
".cm-draftly-table-rendered": {
|
|
777
|
+
padding: "0 !important",
|
|
778
|
+
},
|
|
779
|
+
|
|
780
|
+
".cm-draftly-table-rendered br": {
|
|
781
|
+
display: "none",
|
|
782
|
+
},
|
|
783
|
+
|
|
784
|
+
// Rendered table widget container
|
|
785
|
+
".cm-draftly-table-widget": {
|
|
786
|
+
cursor: "pointer",
|
|
787
|
+
overflow: "auto",
|
|
788
|
+
padding: "0.5rem 0",
|
|
789
|
+
},
|
|
790
|
+
|
|
791
|
+
// Table element
|
|
792
|
+
".cm-draftly-table": {
|
|
793
|
+
width: "100%",
|
|
794
|
+
borderCollapse: "separate",
|
|
795
|
+
borderSpacing: "0",
|
|
796
|
+
borderRadius: "0.5rem",
|
|
797
|
+
overflow: "hidden",
|
|
798
|
+
border: "1px solid var(--color-border, #e2e8f0)",
|
|
799
|
+
fontFamily: "var(--font-sans, sans-serif)",
|
|
800
|
+
fontSize: "0.9375rem",
|
|
801
|
+
lineHeight: "1.5",
|
|
802
|
+
},
|
|
803
|
+
|
|
804
|
+
// Table header
|
|
805
|
+
".cm-draftly-table thead th": {
|
|
806
|
+
padding: "0rem 0.875rem",
|
|
807
|
+
fontWeight: "600",
|
|
808
|
+
borderBottom: "2px solid var(--color-border, #e2e8f0)",
|
|
809
|
+
backgroundColor: "rgba(0, 0, 0, 0.03)",
|
|
810
|
+
},
|
|
811
|
+
|
|
812
|
+
// Table cells
|
|
813
|
+
".cm-draftly-table td": {
|
|
814
|
+
padding: "0rem 0.875rem",
|
|
815
|
+
borderBottom: "1px solid var(--color-border, #e2e8f0)",
|
|
816
|
+
borderRight: "1px solid var(--color-border, #e2e8f0)",
|
|
817
|
+
},
|
|
818
|
+
|
|
819
|
+
// Remove right border on last cell
|
|
820
|
+
".cm-draftly-table td:last-child, .cm-draftly-table th:last-child": {
|
|
821
|
+
borderRight: "none",
|
|
822
|
+
},
|
|
823
|
+
|
|
824
|
+
// Remove bottom border on last row
|
|
825
|
+
".cm-draftly-table tbody tr:last-child td": {
|
|
826
|
+
borderBottom: "none",
|
|
827
|
+
},
|
|
828
|
+
|
|
829
|
+
// Alternate row colors
|
|
830
|
+
".cm-draftly-table tbody tr:nth-child(even)": {
|
|
831
|
+
backgroundColor: "rgba(0, 0, 0, 0.02)",
|
|
832
|
+
},
|
|
833
|
+
|
|
834
|
+
// Header cells right border
|
|
835
|
+
".cm-draftly-table thead th:not(:last-child)": {
|
|
836
|
+
borderRight: "1px solid var(--color-border, #e2e8f0)",
|
|
837
|
+
},
|
|
838
|
+
|
|
839
|
+
// Hover effect on rows
|
|
840
|
+
".cm-draftly-table tbody tr:hover": {
|
|
841
|
+
backgroundColor: "rgba(0, 0, 0, 0.04)",
|
|
842
|
+
},
|
|
843
|
+
|
|
844
|
+
// Inline code in table cells
|
|
845
|
+
".cm-draftly-table-inline-code": {
|
|
846
|
+
fontFamily: "var(--font-jetbrains-mono, monospace)",
|
|
847
|
+
fontSize: "0.85em",
|
|
848
|
+
padding: "0.1em 0.35em",
|
|
849
|
+
borderRadius: "0.25rem",
|
|
850
|
+
backgroundColor: "rgba(0, 0, 0, 0.06)",
|
|
851
|
+
},
|
|
852
|
+
|
|
853
|
+
// Links in table cells
|
|
854
|
+
".cm-draftly-table-link": {
|
|
855
|
+
color: "var(--color-link, #0969da)",
|
|
856
|
+
textDecoration: "none",
|
|
857
|
+
},
|
|
858
|
+
|
|
859
|
+
".cm-draftly-table-link:hover": {
|
|
860
|
+
textDecoration: "underline",
|
|
861
|
+
},
|
|
862
|
+
|
|
863
|
+
// Math in table cells
|
|
864
|
+
".cm-draftly-table-math": {
|
|
865
|
+
fontFamily: "var(--font-jetbrains-mono, monospace)",
|
|
866
|
+
fontSize: "0.9em",
|
|
867
|
+
color: "#6a737d",
|
|
868
|
+
},
|
|
869
|
+
},
|
|
870
|
+
|
|
871
|
+
dark: {
|
|
872
|
+
".cm-draftly-table-line": {
|
|
873
|
+
backgroundColor: "rgba(255, 255, 255, 0.03)",
|
|
874
|
+
},
|
|
875
|
+
|
|
876
|
+
".cm-draftly-table thead th": {
|
|
877
|
+
backgroundColor: "rgba(255, 255, 255, 0.05)",
|
|
878
|
+
},
|
|
879
|
+
|
|
880
|
+
".cm-draftly-table tbody tr:nth-child(even)": {
|
|
881
|
+
backgroundColor: "rgba(255, 255, 255, 0.02)",
|
|
882
|
+
},
|
|
883
|
+
|
|
884
|
+
".cm-draftly-table tbody tr:hover": {
|
|
885
|
+
backgroundColor: "rgba(255, 255, 255, 0.05)",
|
|
886
|
+
},
|
|
887
|
+
|
|
888
|
+
".cm-draftly-table-inline-code": {
|
|
889
|
+
backgroundColor: "rgba(255, 255, 255, 0.08)",
|
|
890
|
+
},
|
|
891
|
+
|
|
892
|
+
".cm-draftly-table-link": {
|
|
893
|
+
color: "var(--color-link, #58a6ff)",
|
|
894
|
+
},
|
|
895
|
+
|
|
896
|
+
".cm-draftly-table-math": {
|
|
897
|
+
color: "#8b949e",
|
|
898
|
+
},
|
|
899
|
+
},
|
|
900
|
+
});
|